diff --git a/aws/rust-runtime/aws-http/Cargo.toml b/aws/rust-runtime/aws-http/Cargo.toml index be5cedd5d87..f09f8bc8e31 100644 --- a/aws/rust-runtime/aws-http/Cargo.toml +++ b/aws/rust-runtime/aws-http/Cargo.toml @@ -28,7 +28,6 @@ aws-smithy-checksums = { path = "../../../rust-runtime/aws-smithy-checksums" } aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" } bytes-utils = "0.1.2" env_logger = "0.9" -http = "0.2.3" tokio = { version = "1.23.1", features = ["macros", "rt", "rt-multi-thread", "test-util", "time"] } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } proptest = "1" diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index 7fa3274cb4d..ccb3d455423 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -18,6 +18,7 @@ aws-types = { path = "../aws-types" } http = "0.2.3" percent-encoding = "2.1.0" tracing = "0.1" +uuid = { version = "1", features = ["v4", "fast-rng"] } [dev-dependencies] aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" } diff --git a/aws/rust-runtime/aws-runtime/external-types.toml b/aws/rust-runtime/aws-runtime/external-types.toml index 90b77e92e01..a38a0e0b57b 100644 --- a/aws/rust-runtime/aws-runtime/external-types.toml +++ b/aws/rust-runtime/aws-runtime/external-types.toml @@ -1,7 +1,8 @@ allowed_external_types = [ "aws_credential_types::*", "aws_sigv4::*", - "aws_smithy_http::body::SdkBody", + "aws_smithy_http::*", + "aws_smithy_types::*", "aws_smithy_runtime_api::*", "aws_types::*", "http::request::Request", diff --git a/aws/rust-runtime/aws-runtime/src/auth.rs b/aws/rust-runtime/aws-runtime/src/auth.rs index 503a4bf2afa..e2b355f0be6 100644 --- a/aws/rust-runtime/aws-runtime/src/auth.rs +++ b/aws/rust-runtime/aws-runtime/src/auth.rs @@ -12,10 +12,9 @@ pub mod sigv4 { UriPathNormalizationMode, }; use aws_smithy_http::property_bag::PropertyBag; - use aws_smithy_runtime_api::client::identity::Identity; + use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver, IdentityResolvers}; use aws_smithy_runtime_api::client::orchestrator::{ - BoxError, HttpAuthScheme, HttpRequest, HttpRequestSigner, IdentityResolver, - IdentityResolvers, + BoxError, HttpAuthScheme, HttpRequest, HttpRequestSigner, }; use aws_types::region::SigningRegion; use aws_types::SigningService; diff --git a/aws/rust-runtime/aws-runtime/src/identity.rs b/aws/rust-runtime/aws-runtime/src/identity.rs index 0cf5b5101eb..2e2eff1bf24 100644 --- a/aws/rust-runtime/aws-runtime/src/identity.rs +++ b/aws/rust-runtime/aws-runtime/src/identity.rs @@ -7,10 +7,8 @@ pub mod credentials { use aws_credential_types::cache::SharedCredentialsCache; use aws_smithy_http::property_bag::PropertyBag; - use aws_smithy_runtime_api::client::identity::Identity; - use aws_smithy_runtime_api::client::orchestrator::{ - BoxError, BoxFallibleFut, IdentityResolver, - }; + use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver}; + use aws_smithy_runtime_api::client::orchestrator::{BoxError, Future}; /// Smithy identity resolver for AWS credentials. #[derive(Debug)] @@ -26,13 +24,13 @@ pub mod credentials { } impl IdentityResolver for CredentialsIdentityResolver { - fn resolve_identity(&self, _identity_properties: &PropertyBag) -> BoxFallibleFut { + fn resolve_identity(&self, _identity_properties: &PropertyBag) -> Future { let cache = self.credentials_cache.clone(); - Box::pin(async move { + Future::new(Box::pin(async move { let credentials = cache.as_ref().provide_cached_credentials().await?; let expiration = credentials.expiry(); Result::<_, BoxError>::Ok(Identity::new(credentials, expiration)) - }) + })) } } } diff --git a/aws/rust-runtime/aws-runtime/src/invocation_id.rs b/aws/rust-runtime/aws-runtime/src/invocation_id.rs new file mode 100644 index 00000000000..ecdf16ed5f7 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/invocation_id.rs @@ -0,0 +1,91 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_runtime_api::client::interceptors::error::BoxError; +use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext}; +use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse}; +use aws_smithy_runtime_api::config_bag::ConfigBag; +use http::{HeaderName, HeaderValue}; +use uuid::Uuid; + +#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this +const AMZ_SDK_INVOCATION_ID: HeaderName = HeaderName::from_static("amz-sdk-invocation-id"); + +/// This interceptor generates a UUID and attaches it to all request attempts made as part of this operation. +#[non_exhaustive] +#[derive(Debug)] +pub struct InvocationIdInterceptor { + id: HeaderValue, +} + +impl InvocationIdInterceptor { + /// Creates a new `InvocationIdInterceptor` + pub fn new() -> Self { + Self::default() + } +} + +impl Default for InvocationIdInterceptor { + fn default() -> Self { + let id = Uuid::new_v4(); + let id = id + .to_string() + .parse() + .expect("UUIDs always produce a valid header value"); + Self { id } + } +} + +impl Interceptor for InvocationIdInterceptor { + fn modify_before_retry_loop( + &self, + context: &mut InterceptorContext, + _cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + let headers = context.request_mut()?.headers_mut(); + headers.append(AMZ_SDK_INVOCATION_ID, self.id.clone()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::invocation_id::InvocationIdInterceptor; + use aws_smithy_http::body::SdkBody; + use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext}; + use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse}; + use aws_smithy_runtime_api::config_bag::ConfigBag; + use aws_smithy_runtime_api::type_erasure::TypedBox; + use http::HeaderValue; + + fn expect_header<'a>( + context: &'a InterceptorContext, + header_name: &str, + ) -> &'a HeaderValue { + context + .request() + .unwrap() + .headers() + .get(header_name) + .unwrap() + } + + #[test] + fn test_id_is_generated_and_set() { + let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase()); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let mut config = ConfigBag::base(); + let interceptor = InvocationIdInterceptor::new(); + interceptor + .modify_before_retry_loop(&mut context, &mut config) + .unwrap(); + + let header = expect_header(&context, "amz-sdk-invocation-id"); + assert_eq!(&interceptor.id, header); + // UUID should include 32 chars and 4 dashes + assert_eq!(interceptor.id.len(), 36); + } +} diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index 31122794317..9df20ae3bcf 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -24,3 +24,9 @@ pub mod recursion_detection; /// Supporting code for user agent headers in the AWS SDK. pub mod user_agent; + +/// Supporting code for retry behavior specific to the AWS SDK. +pub mod retries; + +/// Supporting code for invocation ID headers in the AWS SDK. +pub mod invocation_id; diff --git a/aws/rust-runtime/aws-runtime/src/retries.rs b/aws/rust-runtime/aws-runtime/src/retries.rs new file mode 100644 index 00000000000..ed12aa9cde6 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/retries.rs @@ -0,0 +1,7 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// Classifiers that can inspect a response and determine if it should be retried. +pub mod classifier; diff --git a/aws/rust-runtime/aws-runtime/src/retries/classifier.rs b/aws/rust-runtime/aws-runtime/src/retries/classifier.rs new file mode 100644 index 00000000000..7e9c10d59b1 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/retries/classifier.rs @@ -0,0 +1,174 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_http::http::HttpHeaders; +use aws_smithy_http::result::SdkError; +use aws_smithy_runtime_api::client::retries::RetryReason; +use aws_smithy_types::error::metadata::ProvideErrorMetadata; +use aws_smithy_types::retry::ErrorKind; + +/// AWS error codes that represent throttling errors. +pub const THROTTLING_ERRORS: &[&str] = &[ + "Throttling", + "ThrottlingException", + "ThrottledException", + "RequestThrottledException", + "TooManyRequestsException", + "ProvisionedThroughputExceededException", + "TransactionInProgressException", + "RequestLimitExceeded", + "BandwidthLimitExceeded", + "LimitExceededException", + "RequestThrottled", + "SlowDown", + "PriorRequestNotComplete", + "EC2ThrottledException", +]; + +/// AWS error codes that represent transient errors. +pub const TRANSIENT_ERRORS: &[&str] = &["RequestTimeout", "RequestTimeoutException"]; + +/// A retry classifier for determining if the response sent by an AWS service requires a retry. +#[derive(Debug)] +pub struct AwsErrorCodeClassifier; + +impl AwsErrorCodeClassifier { + /// Classify an error code to check if represents a retryable error. The codes of retryable + /// errors are defined [here](THROTTLING_ERRORS) and [here](TRANSIENT_ERRORS). + pub fn classify_error( + &self, + error: &SdkError, + ) -> Option { + if let Some(error_code) = error.code() { + if THROTTLING_ERRORS.contains(&error_code) { + return Some(RetryReason::Error(ErrorKind::ThrottlingError)); + } else if TRANSIENT_ERRORS.contains(&error_code) { + return Some(RetryReason::Error(ErrorKind::TransientError)); + } + }; + + None + } +} + +/// A retry classifier that checks for `x-amz-retry-after` headers. If one is found, a +/// [`RetryReason::Explicit`] is returned containing the duration to wait before retrying. +#[derive(Debug)] +pub struct AmzRetryAfterHeaderClassifier; + +impl AmzRetryAfterHeaderClassifier { + /// Classify an AWS responses error code to determine how (and if) it should be retried. + pub fn classify_error(&self, error: &SdkError) -> Option { + error + .raw_response() + .and_then(|res| res.http_headers().get("x-amz-retry-after")) + .and_then(|header| header.to_str().ok()) + .and_then(|header| header.parse::().ok()) + .map(|retry_after_delay| { + RetryReason::Explicit(std::time::Duration::from_millis(retry_after_delay)) + }) + } +} + +#[cfg(test)] +mod test { + use super::{AmzRetryAfterHeaderClassifier, AwsErrorCodeClassifier}; + use aws_smithy_http::body::SdkBody; + use aws_smithy_http::operation; + use aws_smithy_http::result::SdkError; + use aws_smithy_runtime_api::client::retries::RetryReason; + use aws_smithy_types::error::metadata::ProvideErrorMetadata; + use aws_smithy_types::error::ErrorMetadata; + use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind}; + use std::fmt; + use std::time::Duration; + + #[derive(Debug)] + struct UnmodeledError; + + impl fmt::Display for UnmodeledError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UnmodeledError") + } + } + + impl std::error::Error for UnmodeledError {} + + struct CodedError { + metadata: ErrorMetadata, + } + + impl CodedError { + fn new(code: &'static str) -> Self { + Self { + metadata: ErrorMetadata::builder().code(code).build(), + } + } + } + + impl ProvideErrorKind for UnmodeledError { + fn retryable_error_kind(&self) -> Option { + None + } + + fn code(&self) -> Option<&str> { + None + } + } + + impl ProvideErrorMetadata for CodedError { + fn meta(&self) -> &ErrorMetadata { + &self.metadata + } + } + + #[test] + fn classify_by_error_code() { + let policy = AwsErrorCodeClassifier; + let res = http::Response::new("OK"); + let err = SdkError::service_error(CodedError::new("Throttling"), res); + + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::ThrottlingError)) + ); + + let res = http::Response::new("OK"); + let err = SdkError::service_error(CodedError::new("RequestTimeout"), res); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::TransientError)) + ) + } + + #[test] + fn classify_generic() { + let policy = AwsErrorCodeClassifier; + let res = http::Response::new("OK"); + let err = aws_smithy_types::Error::builder().code("SlowDown").build(); + let err = SdkError::service_error(err, res); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::ThrottlingError)) + ); + } + + #[test] + fn test_retry_after_header() { + let policy = AmzRetryAfterHeaderClassifier; + let res = http::Response::builder() + .header("x-amz-retry-after", "5000") + .body("retry later") + .unwrap() + .map(SdkBody::from); + let res = operation::Response::new(res); + let err = SdkError::service_error(UnmodeledError, res); + + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Explicit(Duration::from_millis(5000))), + ); + } +} 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 7c245bb7606..6d445f271de 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 @@ -53,6 +53,7 @@ val DECORATORS: List = listOf( AwsRequestIdDecorator(), DisabledAuthDecorator(), RecursionDetectionDecorator(), + InvocationIdDecorator(), ), // Service specific decorators diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt index 5fbe721e1e3..cca5c9f910e 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt @@ -5,7 +5,6 @@ package software.amazon.smithy.rustsdk -import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator @@ -14,6 +13,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.generators.client.Fluen import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientGenerator import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientGenerics import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientSection +import software.amazon.smithy.rust.codegen.client.smithy.generators.client.NoClientGenerics import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.Feature import software.amazon.smithy.rust.codegen.core.rustlang.GenericTypeArg @@ -50,37 +50,6 @@ private class Types(runtimeConfig: RuntimeConfig) { val timeoutConfig = smithyTypes.resolve("timeout::TimeoutConfig") } -private class AwsClientGenerics(private val types: Types) : FluentClientGenerics { - /** Declaration with defaults set */ - override val decl = writable { } - - /** Instantiation of the Smithy client generics */ - override val smithyInst = writable { - rustTemplate( - "<#{DynConnector}, #{DynMiddleware}<#{DynConnector}>>", - "DynConnector" to types.dynConnector, - "DynMiddleware" to types.dynMiddleware, - ) - } - - /** Instantiation */ - override val inst = "" - - /** Trait bounds */ - override val bounds = writable { } - - /** Bounds for generated `send()` functions */ - override fun sendBounds( - operation: Symbol, - operationOutput: Symbol, - operationError: Symbol, - retryClassifier: RuntimeType, - ): Writable = - writable { } - - override fun toRustGenerics() = RustGenerics() -} - class AwsFluentClientDecorator : ClientCodegenDecorator { override val name: String = "FluentClient" @@ -90,7 +59,7 @@ class AwsFluentClientDecorator : ClientCodegenDecorator { override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) { val runtimeConfig = codegenContext.runtimeConfig val types = Types(runtimeConfig) - val generics = AwsClientGenerics(types) + val generics = NoClientGenerics(runtimeConfig) FluentClientGenerator( codegenContext, reexportSmithyClientBuilder = false, 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 c07590f1e28..84d644a43ca 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 @@ -112,18 +112,24 @@ class CredentialsIdentityResolverRegistration( override fun section(section: ServiceRuntimePluginSection): Writable = writable { when (section) { - is ServiceRuntimePluginSection.IdentityResolver -> { + is ServiceRuntimePluginSection.AdditionalConfig -> { rustTemplate( """ - .identity_resolver( - #{SIGV4_SCHEME_ID}, - #{CredentialsIdentityResolver}::new(self.handle.conf.credentials_cache()) - ) + cfg.set_identity_resolvers( + #{IdentityResolvers}::builder() + .identity_resolver( + #{SIGV4_SCHEME_ID}, + #{CredentialsIdentityResolver}::new(self.handle.conf.credentials_cache()) + ) + .build() + ); """, "SIGV4_SCHEME_ID" to AwsRuntimeType.awsRuntime(runtimeConfig) .resolve("auth::sigv4::SCHEME_ID"), "CredentialsIdentityResolver" to AwsRuntimeType.awsRuntime(runtimeConfig) .resolve("identity::credentials::CredentialsIdentityResolver"), + "IdentityResolvers" to RuntimeType.smithyRuntimeApi(runtimeConfig) + .resolve("client::identity::IdentityResolvers"), ) } else -> {} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpConnectorConfigCustomization.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpConnectorConfigCustomization.kt index fcf3cc0c2bd..75c252d0fbc 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpConnectorConfigCustomization.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpConnectorConfigCustomization.kt @@ -15,7 +15,9 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.util.letIf +// TODO(enableNewSmithyRuntime): Delete this decorator since it's now in `codegen-client` class HttpConnectorDecorator : ClientCodegenDecorator { override val name: String = "HttpConnectorDecorator" override val order: Byte = 0 @@ -23,9 +25,10 @@ class HttpConnectorDecorator : ClientCodegenDecorator { override fun configCustomizations( codegenContext: ClientCodegenContext, baseCustomizations: List, - ): List { - return baseCustomizations + HttpConnectorConfigCustomization(codegenContext) - } + ): List = + baseCustomizations.letIf(!codegenContext.settings.codegenConfig.enableNewSmithyRuntime) { + it + HttpConnectorConfigCustomization(codegenContext) + } } class HttpConnectorConfigCustomization( diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/InvocationIdDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/InvocationIdDecorator.kt new file mode 100644 index 00000000000..abf317570fc --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/InvocationIdDecorator.kt @@ -0,0 +1,43 @@ +/* + * 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.ServiceRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginSection +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.util.letIf + +class InvocationIdDecorator : ClientCodegenDecorator { + override val name: String get() = "InvocationIdDecorator" + override val order: Byte get() = 0 + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations.letIf(codegenContext.settings.codegenConfig.enableNewSmithyRuntime) { + it + listOf(InvocationIdRuntimePluginCustomization(codegenContext)) + } +} + +private class InvocationIdRuntimePluginCustomization( + private val codegenContext: ClientCodegenContext, +) : ServiceRuntimePluginCustomization() { + override fun section(section: ServiceRuntimePluginSection): Writable = writable { + if (section is ServiceRuntimePluginSection.AdditionalConfig) { + section.registerInterceptor(codegenContext.runtimeConfig, this) { + rust( + "#T::new()", + AwsRuntimeType.awsRuntime(codegenContext.runtimeConfig) + .resolve("invocation_id::InvocationIdInterceptor"), + ) + } + } + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt index 0d488fb21f2..ef730207a73 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RetryClassifierDecorator.kt @@ -8,7 +8,12 @@ package software.amazon.smithy.rustsdk import software.amazon.smithy.model.shapes.OperationShape 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.OperationRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationRuntimePluginSection +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.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.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType @@ -23,13 +28,21 @@ class RetryClassifierDecorator : ClientCodegenDecorator { codegenContext: ClientCodegenContext, operation: OperationShape, baseCustomizations: List, - ): List { - return baseCustomizations + RetryClassifierFeature(codegenContext.runtimeConfig) - } + ): List = + baseCustomizations + RetryClassifierFeature(codegenContext.runtimeConfig) + + override fun operationRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + operation: OperationShape, + baseCustomizations: List, + ): List = + baseCustomizations + OperationRetryClassifiersFeature(codegenContext, operation) } class RetryClassifierFeature(private val runtimeConfig: RuntimeConfig) : OperationCustomization() { - override fun retryType(): RuntimeType = AwsRuntimeType.awsHttp(runtimeConfig).resolve("retry::AwsResponseRetryClassifier") + override fun retryType(): RuntimeType = + AwsRuntimeType.awsHttp(runtimeConfig).resolve("retry::AwsResponseRetryClassifier") + override fun section(section: OperationSection) = when (section) { is OperationSection.FinalizeOperation -> writable { rust( @@ -41,3 +54,140 @@ class RetryClassifierFeature(private val runtimeConfig: RuntimeConfig) : Operati else -> emptySection } } + +class OperationRetryClassifiersFeature( + codegenContext: ClientCodegenContext, + operation: OperationShape, +) : OperationRuntimePluginCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig) + private val smithyRuntime = RuntimeType.smithyRuntime(runtimeConfig) + private val smithyRuntimeApi = RuntimeType.smithyRuntimeApi(runtimeConfig) + private val codegenScope = arrayOf( + "HttpStatusCodeClassifier" to smithyRuntime.resolve("client::retries::classifier::HttpStatusCodeClassifier"), + "AwsErrorCodeClassifier" to awsRuntime.resolve("retries::classifier::AwsErrorCodeClassifier"), + "ModeledAsRetryableClassifier" to smithyRuntime.resolve("client::retries::classifier::ModeledAsRetryableClassifier"), + "AmzRetryAfterHeaderClassifier" to awsRuntime.resolve("retries::classifier::AmzRetryAfterHeaderClassifier"), + "SmithyErrorClassifier" to smithyRuntime.resolve("client::retries::classifier::SmithyErrorClassifier"), + "RetryReason" to smithyRuntimeApi.resolve("client::retries::RetryReason"), + "ClassifyRetry" to smithyRuntimeApi.resolve("client::retries::ClassifyRetry"), + "RetryClassifiers" to smithyRuntimeApi.resolve("client::retries::RetryClassifiers"), + "OperationError" to codegenContext.symbolProvider.symbolForOperationError(operation), + "SdkError" to RuntimeType.smithyHttp(runtimeConfig).resolve("result::SdkError"), + "ErasedError" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("type_erasure::TypeErasedBox"), + ) + + override fun section(section: OperationRuntimePluginSection) = when (section) { + is OperationRuntimePluginSection.RuntimePluginSupportingTypes -> writable { + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct HttpStatusCodeClassifier(#{HttpStatusCodeClassifier}); + impl HttpStatusCodeClassifier { + fn new() -> Self { + Self(#{HttpStatusCodeClassifier}::default()) + } + } + impl #{ClassifyRetry} for HttpStatusCodeClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct AwsErrorCodeClassifier(#{AwsErrorCodeClassifier}); + impl AwsErrorCodeClassifier { + fn new() -> Self { + Self(#{AwsErrorCodeClassifier}) + } + } + impl #{ClassifyRetry} for AwsErrorCodeClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct ModeledAsRetryableClassifier(#{ModeledAsRetryableClassifier}); + impl ModeledAsRetryableClassifier { + fn new() -> Self { + Self(#{ModeledAsRetryableClassifier}) + } + } + impl #{ClassifyRetry} for ModeledAsRetryableClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct AmzRetryAfterHeaderClassifier(#{AmzRetryAfterHeaderClassifier}); + impl AmzRetryAfterHeaderClassifier { + fn new() -> Self { + Self(#{AmzRetryAfterHeaderClassifier}) + } + } + impl #{ClassifyRetry} for AmzRetryAfterHeaderClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + + Attribute(derive(RuntimeType.Debug)).render(this) + rustTemplate( + """ + struct SmithyErrorClassifier(#{SmithyErrorClassifier}); + impl SmithyErrorClassifier { + fn new() -> Self { + Self(#{SmithyErrorClassifier}) + } + } + impl #{ClassifyRetry} for SmithyErrorClassifier { + fn classify_retry(&self, error: &#{ErasedError}) -> Option<#{RetryReason}> { + let error = error.downcast_ref::<#{SdkError}<#{OperationError}>>().expect("The error type is always known"); + self.0.classify_error(error) + } + } + """, + *codegenScope, + ) + } + + is OperationRuntimePluginSection.RetryClassifier -> writable { + rustTemplate( + """ + .with_classifier(SmithyErrorClassifier::new()) + .with_classifier(AmzRetryAfterHeaderClassifier::new()) + .with_classifier(ModeledAsRetryableClassifier::new()) + .with_classifier(AwsErrorCodeClassifier::new()) + .with_classifier(HttpStatusCodeClassifier::new()) + """, + *codegenScope, + ) + } + + else -> emptySection + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt index 751788df291..7ae7582e69e 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt @@ -136,6 +136,7 @@ private class AuthOperationRuntimePluginCustomization(private val codegenContext signing_options.normalize_uri_path = $normalizeUrlPath; signing_options.signing_optional = $signingOptional; signing_options.payload_override = #{payload_override}; + signing_options.request_timestamp = cfg.request_time().unwrap_or_default().system_time(); let mut sigv4_properties = #{PropertyBag}::new(); sigv4_properties.insert(#{SigV4OperationSigningConfig} { diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/benches/middleware_vs_orchestrator.rs b/aws/sra-test/integration-tests/aws-sdk-s3/benches/middleware_vs_orchestrator.rs index 417b99c77b3..d587cdb0b45 100644 --- a/aws/sra-test/integration-tests/aws-sdk-s3/benches/middleware_vs_orchestrator.rs +++ b/aws/sra-test/integration-tests/aws-sdk-s3/benches/middleware_vs_orchestrator.rs @@ -135,8 +135,9 @@ mod orchestrator { use aws_smithy_http::endpoint::SharedEndpointResolver; use aws_smithy_runtime::client::connections::adapter::DynConnectorAdapter; use aws_smithy_runtime::client::orchestrator::endpoints::DefaultEndpointResolver; + use aws_smithy_runtime_api::client::interceptors::error::ContextAttachedError; use aws_smithy_runtime_api::client::interceptors::{ - Interceptor, InterceptorContext, InterceptorError, Interceptors, + Interceptor, InterceptorContext, Interceptors, }; use aws_smithy_runtime_api::client::orchestrator::{ BoxError, ConfigBagAccessors, Connection, HttpRequest, HttpResponse, TraceProbe, @@ -156,7 +157,7 @@ mod orchestrator { impl RuntimePlugin for ManualServiceRuntimePlugin { fn configure(&self, cfg: &mut ConfigBag) -> Result<(), BoxError> { let identity_resolvers = - aws_smithy_runtime_api::client::orchestrator::IdentityResolvers::builder() + aws_smithy_runtime_api::client::identity::IdentityResolvers::builder() .identity_resolver( aws_runtime::auth::sigv4::SCHEME_ID, aws_runtime::identity::credentials::CredentialsIdentityResolver::new( diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/tests/interceptors.rs b/aws/sra-test/integration-tests/aws-sdk-s3/tests/interceptors.rs deleted file mode 100644 index fd4c8649c04..00000000000 --- a/aws/sra-test/integration-tests/aws-sdk-s3/tests/interceptors.rs +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// type TxReq = http::Request; -// type TxRes = http::Response; -// -// pub struct SigV4SigningConfigInterceptor { -// pub signing_service: &'static str, -// pub signing_region: Option, -// } - -// // Mount the interceptors -// let mut interceptors = Interceptors::new(); -// let sig_v4_signing_config_interceptor = SigV4SigningConfigInterceptor { -// signing_region: service_config.region.clone(), -// signing_service: service_config.signing_service(), -// }; -// let credentials_cache_interceptor = CredentialsCacheInterceptor { -// shared_credentials_cache: service_config.credentials_cache.clone(), -// }; -// let checksum_interceptor = ChecksumInterceptor { -// checksum_mode: input.checksum_mode().cloned(), -// }; -// interceptors -// .with_interceptor(sig_v4_signing_config_interceptor) -// .with_interceptor(credentials_cache_interceptor) -// .with_interceptor(checksum_interceptor); - -// let token_bucket = Box::new(standard::TokenBucket::builder().max_tokens(500).build()); -// -// impl Interceptor for SigV4SigningConfigInterceptor { -// fn modify_before_signing( -// &mut self, -// context: &mut InterceptorContext, -// ) -> Result<(), InterceptorError> { -// let mut props = context.properties_mut(); -// -// let mut signing_config = OperationSigningConfig::default_config(); -// signing_config.signing_options.content_sha256_header = true; -// signing_config.signing_options.double_uri_encode = false; -// signing_config.signing_options.normalize_uri_path = false; -// props.insert(signing_config); -// props.insert(aws_types::SigningService::from_static(self.signing_service)); -// -// if let Some(signing_region) = self.signing_region.as_ref() { -// props.insert(aws_types::region::SigningRegion::from( -// signing_region.clone(), -// )); -// } -// -// Ok(()) -// } -// } -// -// pub struct CredentialsCacheInterceptor { -// pub shared_credentials_cache: SharedCredentialsCache, -// } -// -// impl Interceptor for CredentialsCacheInterceptor { -// fn modify_before_signing( -// &mut self, -// context: &mut InterceptorContext, -// ) -> Result<(), InterceptorError> { -// match self -// .shared_credentials_cache -// .as_ref() -// .provide_cached_credentials() -// .now_or_never() -// { -// Some(Ok(creds)) => { -// context.properties_mut().insert(creds); -// } -// // ignore the case where there is no credentials cache wired up -// Some(Err(CredentialsError::CredentialsNotLoaded { .. })) => { -// tracing::info!("credentials cache returned CredentialsNotLoaded, ignoring") -// } -// // if we get another error class, there is probably something actually wrong that the user will -// // want to know about -// Some(Err(other)) => return Err(InterceptorError::ModifyBeforeSigning(other.into())), -// None => unreachable!("fingers crossed that creds are always available"), -// } -// -// Ok(()) -// } -// } -// -// pub struct ChecksumInterceptor { -// pub checksum_mode: Option, -// } -// -// impl Interceptor for ChecksumInterceptor { -// fn modify_before_serialization( -// &mut self, -// context: &mut InterceptorContext, -// ) -> Result<(), InterceptorError> { -// let mut props = context.properties_mut(); -// props.insert(self.checksum_mode.clone()); -// -// Ok(()) -// } -// } diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs b/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs index 77d576edfe9..28d8d9a3b72 100644 --- a/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs +++ b/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs @@ -6,7 +6,6 @@ use aws_credential_types::cache::{CredentialsCache, SharedCredentialsCache}; use aws_credential_types::provider::SharedCredentialsProvider; use aws_http::user_agent::{ApiMetadata, AwsUserAgent}; -use aws_runtime::auth::sigv4::SigV4OperationSigningConfig; use aws_runtime::recursion_detection::RecursionDetectionInterceptor; use aws_runtime::user_agent::UserAgentInterceptor; use aws_sdk_s3::config::{Credentials, Region}; @@ -23,18 +22,18 @@ use aws_smithy_runtime::client::orchestrator::endpoints::DefaultEndpointResolver use aws_smithy_runtime_api::client::interceptors::error::ContextAttachedError; use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext, Interceptors}; use aws_smithy_runtime_api::client::orchestrator::{ - BoxError, ConfigBagAccessors, Connection, HttpRequest, HttpResponse, TraceProbe, + BoxError, ConfigBagAccessors, Connection, HttpRequest, HttpResponse, RequestTime, TraceProbe, }; +use aws_smithy_runtime_api::client::retries::RetryClassifiers; use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; use aws_smithy_runtime_api::config_bag::ConfigBag; use aws_smithy_runtime_api::type_erasure::TypedBox; use aws_types::region::SigningRegion; use aws_types::SigningService; +use http::Uri; use std::sync::Arc; use std::time::{Duration, UNIX_EPOCH}; -mod interceptors; - // TODO(orchestrator-test): unignore #[ignore] #[tokio::test] @@ -83,21 +82,6 @@ async fn sra_manual_test() { impl RuntimePlugin for ManualServiceRuntimePlugin { fn configure(&self, cfg: &mut ConfigBag) -> Result<(), BoxError> { - let identity_resolvers = - aws_smithy_runtime_api::client::orchestrator::IdentityResolvers::builder() - .identity_resolver( - aws_runtime::auth::sigv4::SCHEME_ID, - aws_runtime::identity::credentials::CredentialsIdentityResolver::new( - self.credentials_cache.clone(), - ), - ) - .identity_resolver( - "anonymous", - aws_smithy_runtime_api::client::identity::AnonymousIdentityResolver::new(), - ) - .build(); - cfg.set_identity_resolvers(identity_resolvers); - let http_auth_schemes = aws_smithy_runtime_api::client::orchestrator::HttpAuthSchemes::builder() .auth_scheme( @@ -107,6 +91,7 @@ async fn sra_manual_test() { .build(); cfg.set_http_auth_schemes(http_auth_schemes); + // Set an empty auth option resolver to be overridden by operations that need auth. cfg.set_auth_option_resolver( aws_smithy_runtime_api::client::auth::option_resolver::AuthOptionListResolver::new( Vec::new(), @@ -121,7 +106,7 @@ async fn sra_manual_test() { cfg.put(params_builder); cfg.set_retry_strategy( - aws_smithy_runtime_api::client::retries::NeverRetryStrategy::new(), + aws_smithy_runtime::client::retries::strategy::StandardRetryStrategy::default(), ); let connection: Box = Box::new(DynConnectorAdapter::new( @@ -142,31 +127,26 @@ async fn sra_manual_test() { cfg.put(SigningService::from_static("s3")); cfg.put(SigningRegion::from(Region::from_static("us-east-1"))); - - #[derive(Debug)] - struct OverrideSigningTimeInterceptor; - impl Interceptor for OverrideSigningTimeInterceptor { - fn read_before_signing( - &self, - _context: &InterceptorContext, - cfg: &mut ConfigBag, - ) -> Result<(), BoxError> { - let mut signing_config = - cfg.get::().unwrap().clone(); - signing_config.signing_options.request_timestamp = - UNIX_EPOCH + Duration::from_secs(1624036048); - cfg.put(signing_config); - Ok(()) - } - } + cfg.set_request_time(RequestTime::new( + UNIX_EPOCH + Duration::from_secs(1624036048), + )); cfg.put(ApiMetadata::new("unused", "unused")); cfg.put(AwsUserAgent::for_tests()); // Override the user agent with the test UA cfg.get::>() .expect("interceptors set") .register_client_interceptor(Arc::new(UserAgentInterceptor::new()) as _) - .register_client_interceptor(Arc::new(RecursionDetectionInterceptor::new()) as _) - .register_client_interceptor(Arc::new(OverrideSigningTimeInterceptor) as _); + .register_client_interceptor(Arc::new(RecursionDetectionInterceptor::new()) as _); + cfg.set_identity_resolvers( + aws_smithy_runtime_api::client::identity::IdentityResolvers::builder() + .identity_resolver( + aws_runtime::auth::sigv4::SCHEME_ID, + aws_runtime::identity::credentials::CredentialsIdentityResolver::new( + self.credentials_cache.clone(), + ), + ) + .build(), + ); Ok(()) } } @@ -192,7 +172,7 @@ async fn sra_manual_test() { .ok_or_else(|| "failed to downcast to ListObjectsV2Input")?; let mut params_builder = cfg .get::() - .ok_or_else(|| "missing endpoint params builder")? + .ok_or("missing endpoint params builder")? .clone(); params_builder = params_builder.set_bucket(input.bucket.clone()); cfg.put(params_builder); @@ -211,7 +191,7 @@ async fn sra_manual_test() { ) -> Result<(), BoxError> { let params_builder = cfg .get::() - .ok_or_else(|| "missing endpoint params builder")? + .ok_or("missing endpoint params builder")? .clone(); let params = params_builder.build().map_err(|err| { ContextAttachedError::new("endpoint params could not be built", err) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt index 2e72185f9d9..faa3a01c5df 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt @@ -11,6 +11,8 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.rust.codegen.client.smithy.customizations.ApiKeyAuthDecorator import software.amazon.smithy.rust.codegen.client.smithy.customizations.ClientCustomizations +import software.amazon.smithy.rust.codegen.client.smithy.customizations.HttpAuthDecorator +import software.amazon.smithy.rust.codegen.client.smithy.customizations.HttpConnectorConfigDecorator import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator import software.amazon.smithy.rust.codegen.client.smithy.customize.CombinedClientCodegenDecorator import software.amazon.smithy.rust.codegen.client.smithy.customize.NoOpEventStreamSigningDecorator @@ -62,6 +64,8 @@ class RustClientCodegenPlugin : ClientDecoratableBuildPlugin() { EndpointParamsDecorator(), NoOpEventStreamSigningDecorator(), ApiKeyAuthDecorator(), + HttpAuthDecorator(), + HttpConnectorConfigDecorator(), *decorator, ) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ApiKeyAuthDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ApiKeyAuthDecorator.kt index 0d9f5bde460..58fd14e6a53 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ApiKeyAuthDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ApiKeyAuthDecorator.kt @@ -26,9 +26,10 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationCustomization import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSection -import software.amazon.smithy.rust.codegen.core.util.expectTrait import software.amazon.smithy.rust.codegen.core.util.letIf +// TODO(enableNewSmithyRuntime): Delete this decorator when switching to the orchestrator + /** * Inserts a ApiKeyAuth configuration into the operation */ @@ -37,7 +38,8 @@ class ApiKeyAuthDecorator : ClientCodegenDecorator { override val order: Byte = 10 private fun applies(codegenContext: ClientCodegenContext) = - isSupportedApiKeyAuth(codegenContext) + !codegenContext.settings.codegenConfig.enableNewSmithyRuntime && + isSupportedApiKeyAuth(codegenContext) override fun configCustomizations( codegenContext: ClientCodegenContext, diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt new file mode 100644 index 00000000000..7f23f7a853f --- /dev/null +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt @@ -0,0 +1,373 @@ +/* + * 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.model.knowledge.ServiceIndex +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.traits.AuthTrait +import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait +import software.amazon.smithy.model.traits.HttpBasicAuthTrait +import software.amazon.smithy.model.traits.HttpBearerAuthTrait +import software.amazon.smithy.model.traits.HttpDigestAuthTrait +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationRuntimePluginSection +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginSection +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.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.withBlockTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.core.util.getTrait +import software.amazon.smithy.rust.codegen.core.util.letIf + +fun codegenScope(runtimeConfig: RuntimeConfig): Array> { + val smithyRuntime = + CargoDependency.smithyRuntime(runtimeConfig).withFeature("http-auth").toType() + val smithyRuntimeApi = CargoDependency.smithyRuntimeApi(runtimeConfig).withFeature("http-auth").toType() + val authHttp = smithyRuntime.resolve("client::auth::http") + val authHttpApi = smithyRuntimeApi.resolve("client::auth::http") + return arrayOf( + "ApiKeyAuthScheme" to authHttp.resolve("ApiKeyAuthScheme"), + "ApiKeyLocation" to authHttp.resolve("ApiKeyLocation"), + "AuthOptionListResolver" to smithyRuntimeApi.resolve("client::auth::option_resolver::AuthOptionListResolver"), + "BasicAuthScheme" to authHttp.resolve("BasicAuthScheme"), + "BearerAuthScheme" to authHttp.resolve("BearerAuthScheme"), + "DigestAuthScheme" to authHttp.resolve("DigestAuthScheme"), + "HTTP_API_KEY_AUTH_SCHEME_ID" to authHttpApi.resolve("HTTP_API_KEY_AUTH_SCHEME_ID"), + "HTTP_BASIC_AUTH_SCHEME_ID" to authHttpApi.resolve("HTTP_BASIC_AUTH_SCHEME_ID"), + "HTTP_BEARER_AUTH_SCHEME_ID" to authHttpApi.resolve("HTTP_BEARER_AUTH_SCHEME_ID"), + "HTTP_DIGEST_AUTH_SCHEME_ID" to authHttpApi.resolve("HTTP_DIGEST_AUTH_SCHEME_ID"), + "HttpAuthOption" to smithyRuntimeApi.resolve("client::orchestrator::HttpAuthOption"), + "IdentityResolver" to smithyRuntimeApi.resolve("client::identity::IdentityResolver"), + "IdentityResolvers" to smithyRuntimeApi.resolve("client::identity::IdentityResolvers"), + "Login" to smithyRuntimeApi.resolve("client::identity::http::Login"), + "PropertyBag" to RuntimeType.smithyHttp(runtimeConfig).resolve("property_bag::PropertyBag"), + "Token" to smithyRuntimeApi.resolve("client::identity::http::Token"), + ) +} + +private data class HttpAuthSchemes( + val apiKey: Boolean, + val basic: Boolean, + val bearer: Boolean, + val digest: Boolean, +) { + companion object { + fun from(codegenContext: ClientCodegenContext): HttpAuthSchemes { + val authSchemes = ServiceIndex.of(codegenContext.model).getAuthSchemes(codegenContext.serviceShape).keys + val newRuntimeEnabled = codegenContext.settings.codegenConfig.enableNewSmithyRuntime + return HttpAuthSchemes( + apiKey = newRuntimeEnabled && authSchemes.contains(HttpApiKeyAuthTrait.ID), + basic = newRuntimeEnabled && authSchemes.contains(HttpBasicAuthTrait.ID), + bearer = newRuntimeEnabled && authSchemes.contains(HttpBearerAuthTrait.ID), + digest = newRuntimeEnabled && authSchemes.contains(HttpDigestAuthTrait.ID), + ) + } + } + + fun anyEnabled(): Boolean = isTokenBased() || isLoginBased() + fun isTokenBased(): Boolean = apiKey || bearer + fun isLoginBased(): Boolean = basic || digest +} + +class HttpAuthDecorator : ClientCodegenDecorator { + override val name: String get() = "HttpAuthDecorator" + override val order: Byte = 0 + + override fun configCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + HttpAuthSchemes.from(codegenContext).let { authSchemes -> + baseCustomizations.letIf(authSchemes.anyEnabled()) { + it + HttpAuthConfigCustomization(codegenContext, authSchemes) + } + } + + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + HttpAuthSchemes.from(codegenContext).let { authSchemes -> + baseCustomizations.letIf(authSchemes.anyEnabled()) { + it + HttpAuthServiceRuntimePluginCustomization(codegenContext, authSchemes) + } + } + + override fun operationRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + operation: OperationShape, + baseCustomizations: List, + ): List = + HttpAuthSchemes.from(codegenContext).let { authSchemes -> + baseCustomizations.letIf(authSchemes.anyEnabled()) { + it + HttpAuthOperationRuntimePluginCustomization(codegenContext) + } + } + + override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) { + val authSchemes = HttpAuthSchemes.from(codegenContext) + if (authSchemes.anyEnabled()) { + rustCrate.withModule(ClientRustModule.Config) { + val codegenScope = codegenScope(codegenContext.runtimeConfig) + if (authSchemes.isTokenBased()) { + rustTemplate("pub use #{Token};", *codegenScope) + } + if (authSchemes.isLoginBased()) { + rustTemplate("pub use #{Login};", *codegenScope) + } + } + } + } +} + +private class HttpAuthServiceRuntimePluginCustomization( + codegenContext: ClientCodegenContext, + private val authSchemes: HttpAuthSchemes, +) : ServiceRuntimePluginCustomization() { + private val serviceShape = codegenContext.serviceShape + private val codegenScope = codegenScope(codegenContext.runtimeConfig) + + override fun section(section: ServiceRuntimePluginSection): Writable = writable { + when (section) { + is ServiceRuntimePluginSection.HttpAuthScheme -> { + if (authSchemes.apiKey) { + val trait = serviceShape.getTrait()!! + val location = when (trait.`in`) { + HttpApiKeyAuthTrait.Location.HEADER -> { + check(trait.scheme.isPresent) { + "A scheme is required for `@httpApiKey` when `in` is set to `header`" + } + "Header" + } + + HttpApiKeyAuthTrait.Location.QUERY -> "Query" + } + + rustTemplate( + """ + .auth_scheme( + #{HTTP_API_KEY_AUTH_SCHEME_ID}, + #{ApiKeyAuthScheme}::new( + ${trait.scheme.orElse("").dq()}, + #{ApiKeyLocation}::$location, + ${trait.name.dq()}, + ) + ) + """, + *codegenScope, + ) + } + if (authSchemes.basic) { + rustTemplate(".auth_scheme(#{HTTP_BASIC_AUTH_SCHEME_ID}, #{BasicAuthScheme}::new())", *codegenScope) + } + if (authSchemes.bearer) { + rustTemplate( + ".auth_scheme(#{HTTP_BEARER_AUTH_SCHEME_ID}, #{BearerAuthScheme}::new())", + *codegenScope, + ) + } + if (authSchemes.digest) { + rustTemplate( + ".auth_scheme(#{HTTP_DIGEST_AUTH_SCHEME_ID}, #{DigestAuthScheme}::new())", + *codegenScope, + ) + } + } + + is ServiceRuntimePluginSection.AdditionalConfig -> { + if (authSchemes.anyEnabled()) { + rust("cfg.set_identity_resolvers(self.handle.conf.identity_resolvers().clone());") + } + } + } + } +} + +private class HttpAuthOperationRuntimePluginCustomization( + codegenContext: ClientCodegenContext, +) : OperationRuntimePluginCustomization() { + private val serviceShape = codegenContext.serviceShape + private val codegenScope = codegenScope(codegenContext.runtimeConfig) + + override fun section(section: OperationRuntimePluginSection): Writable = writable { + when (section) { + is OperationRuntimePluginSection.AdditionalConfig -> { + withBlockTemplate( + "let auth_option_resolver = #{AuthOptionListResolver}::new(vec![", + "]);", + *codegenScope, + ) { + val authTrait: AuthTrait? = section.operationShape.getTrait() ?: serviceShape.getTrait() + for (authScheme in authTrait?.valueSet ?: emptySet()) { + when (authScheme) { + HttpApiKeyAuthTrait.ID -> { + rustTemplate( + "#{HttpAuthOption}::new(#{HTTP_API_KEY_AUTH_SCHEME_ID}, std::sync::Arc::new(#{PropertyBag}::new())),", + *codegenScope, + ) + } + + HttpBasicAuthTrait.ID -> { + rustTemplate( + "#{HttpAuthOption}::new(#{HTTP_BASIC_AUTH_SCHEME_ID}, std::sync::Arc::new(#{PropertyBag}::new())),", + *codegenScope, + ) + } + + HttpBearerAuthTrait.ID -> { + rustTemplate( + "#{HttpAuthOption}::new(#{HTTP_BEARER_AUTH_SCHEME_ID}, std::sync::Arc::new(#{PropertyBag}::new())),", + *codegenScope, + ) + } + + HttpDigestAuthTrait.ID -> { + rustTemplate( + "#{HttpAuthOption}::new(#{HTTP_DIGEST_AUTH_SCHEME_ID}, std::sync::Arc::new(#{PropertyBag}::new())),", + *codegenScope, + ) + } + + else -> {} + } + } + } + + rustTemplate("${section.configBagName}.set_auth_option_resolver(auth_option_resolver);", *codegenScope) + } + + else -> emptySection + } + } +} + +private class HttpAuthConfigCustomization( + codegenContext: ClientCodegenContext, + private val authSchemes: HttpAuthSchemes, +) : ConfigCustomization() { + private val codegenScope = codegenScope(codegenContext.runtimeConfig) + + override fun section(section: ServiceConfig): Writable = writable { + when (section) { + is ServiceConfig.BuilderStruct -> { + rustTemplate("identity_resolvers: #{IdentityResolvers},", *codegenScope) + } + + is ServiceConfig.BuilderImpl -> { + if (authSchemes.apiKey) { + rustTemplate( + """ + /// Sets the API key that will be used for authentication. + pub fn api_key(self, api_key: #{Token}) -> Self { + self.api_key_resolver(api_key) + } + + /// Sets an API key resolver will be used for authentication. + pub fn api_key_resolver(mut self, api_key_resolver: impl #{IdentityResolver} + 'static) -> Self { + self.identity_resolvers = self.identity_resolvers.to_builder() + .identity_resolver(#{HTTP_API_KEY_AUTH_SCHEME_ID}, api_key_resolver) + .build(); + self + } + """, + *codegenScope, + ) + } + if (authSchemes.bearer) { + rustTemplate( + """ + /// Sets the bearer token that will be used for HTTP bearer auth. + pub fn bearer_token(self, bearer_token: #{Token}) -> Self { + self.bearer_token_resolver(bearer_token) + } + + /// Sets a bearer token provider that will be used for HTTP bearer auth. + pub fn bearer_token_resolver(mut self, bearer_token_resolver: impl #{IdentityResolver} + 'static) -> Self { + self.identity_resolvers = self.identity_resolvers.to_builder() + .identity_resolver(#{HTTP_BEARER_AUTH_SCHEME_ID}, bearer_token_resolver) + .build(); + self + } + """, + *codegenScope, + ) + } + if (authSchemes.basic) { + rustTemplate( + """ + /// Sets the login that will be used for HTTP basic auth. + pub fn basic_auth_login(self, basic_auth_login: #{Login}) -> Self { + self.basic_auth_login_resolver(basic_auth_login) + } + + /// Sets a login resolver that will be used for HTTP basic auth. + pub fn basic_auth_login_resolver(mut self, basic_auth_resolver: impl #{IdentityResolver} + 'static) -> Self { + self.identity_resolvers = self.identity_resolvers.to_builder() + .identity_resolver(#{HTTP_BASIC_AUTH_SCHEME_ID}, basic_auth_resolver) + .build(); + self + } + """, + *codegenScope, + ) + } + if (authSchemes.digest) { + rustTemplate( + """ + /// Sets the login that will be used for HTTP digest auth. + pub fn digest_auth_login(self, digest_auth_login: #{Login}) -> Self { + self.digest_auth_login_resolver(digest_auth_login) + } + + /// Sets a login resolver that will be used for HTTP digest auth. + pub fn digest_auth_login_resolver(mut self, digest_auth_resolver: impl #{IdentityResolver} + 'static) -> Self { + self.identity_resolvers = self.identity_resolvers.to_builder() + .identity_resolver(#{HTTP_DIGEST_AUTH_SCHEME_ID}, digest_auth_resolver) + .build(); + self + } + """, + *codegenScope, + ) + } + } + + is ServiceConfig.BuilderBuild -> { + rust("identity_resolvers: self.identity_resolvers,") + } + + is ServiceConfig.ConfigStruct -> { + rustTemplate("identity_resolvers: #{IdentityResolvers},", *codegenScope) + } + + is ServiceConfig.ConfigImpl -> { + rustTemplate( + """ + /// Returns the identity resolvers. + pub fn identity_resolvers(&self) -> &#{IdentityResolvers} { + &self.identity_resolvers + } + """, + *codegenScope, + ) + } + + else -> {} + } + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt new file mode 100644 index 00000000000..102c50e718f --- /dev/null +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt @@ -0,0 +1,157 @@ +/* + * 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.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.Writable +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.CodegenContext +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.util.letIf + +class HttpConnectorConfigDecorator : ClientCodegenDecorator { + override val name: String = "HttpConnectorConfigDecorator" + override val order: Byte = 0 + + override fun configCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations.letIf(codegenContext.settings.codegenConfig.enableNewSmithyRuntime) { + it + HttpConnectorConfigCustomization(codegenContext) + } +} + +private class HttpConnectorConfigCustomization( + codegenContext: CodegenContext, +) : ConfigCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val moduleUseName = codegenContext.moduleUseName() + private val codegenScope = arrayOf( + "HttpConnector" to RuntimeType.smithyClient(runtimeConfig).resolve("http_connector::HttpConnector"), + ) + + override fun section(section: ServiceConfig): Writable { + return when (section) { + is ServiceConfig.ConfigStruct -> writable { + rustTemplate("http_connector: Option<#{HttpConnector}>,", *codegenScope) + } + + is ServiceConfig.ConfigImpl -> writable { + rustTemplate( + """ + /// Return an [`HttpConnector`](#{HttpConnector}) to use when making requests, if any. + pub fn http_connector(&self) -> Option<&#{HttpConnector}> { + self.http_connector.as_ref() + } + """, + *codegenScope, + ) + } + + is ServiceConfig.BuilderStruct -> writable { + rustTemplate("http_connector: Option<#{HttpConnector}>,", *codegenScope) + } + + ServiceConfig.BuilderImpl -> writable { + rustTemplate( + """ + /// Sets the HTTP connector to use when making requests. + /// + /// ## Examples + /// ```no_run + /// ## ##[cfg(test)] + /// ## mod tests { + /// ## ##[test] + /// ## fn example() { + /// use std::time::Duration; + /// use aws_smithy_client::{Client, hyper_ext}; + /// use aws_smithy_client::erase::DynConnector; + /// use aws_smithy_client::http_connector::ConnectorSettings; + /// use $moduleUseName::config::Config; + /// + /// 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); + /// ## } + /// ## } + /// ``` + pub fn http_connector(mut self, http_connector: impl Into<#{HttpConnector}>) -> Self { + self.http_connector = Some(http_connector.into()); + self + } + + /// Sets the HTTP connector to use when making requests. + /// + /// ## Examples + /// ```no_run + /// ## ##[cfg(test)] + /// ## mod tests { + /// ## ##[test] + /// ## fn example() { + /// use std::time::Duration; + /// use aws_smithy_client::hyper_ext; + /// use aws_smithy_client::http_connector::ConnectorSettings; + /// use crate::sdk_config::{SdkConfig, Builder}; + /// use $moduleUseName::config::{Builder, Config}; + /// + /// fn override_http_connector(builder: &mut Builder) { + /// 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); + /// builder.set_http_connector(Some(smithy_connector)); + /// } + /// + /// let mut builder = $moduleUseName::Config::builder(); + /// override_http_connector(&mut builder); + /// let config = builder.build(); + /// ## } + /// ## } + /// ``` + pub fn set_http_connector(&mut self, http_connector: Option>) -> &mut Self { + self.http_connector = http_connector.map(|inner| inner.into()); + self + } + """, + *codegenScope, + ) + } + + is ServiceConfig.BuilderBuild -> writable { + rust("http_connector: self.http_connector,") + } + + else -> emptySection + } + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt index 0299eb21c76..b00a020508b 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointParamsInterceptorGenerator.kt @@ -84,10 +84,10 @@ class EndpointParamsInterceptorGenerator( let input = context.input()?; let _input = input .downcast_ref::<${operationInput.name}>() - .ok_or_else(|| "failed to downcast to ${operationInput.name}")?; + .ok_or("failed to downcast to ${operationInput.name}")?; let params_builder = cfg .get::<#{ParamsBuilder}>() - .ok_or_else(|| "missing endpoint params builder")? + .ok_or("missing endpoint params builder")? .clone(); ${"" /* TODO(EndpointResolver): Call setters on `params_builder` to update its fields by using values from `_input` */} cfg.put(params_builder); @@ -131,7 +131,7 @@ class EndpointParamsInterceptorGenerator( let _ = context; let params_builder = cfg .get::<#{ParamsBuilder}>() - .ok_or_else(|| "missing endpoint params builder")? + .ok_or("missing endpoint params builder")? .clone(); let params = params_builder .build() diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt index 2ad5d454a2c..7c2d1e76626 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt @@ -40,6 +40,29 @@ sealed class OperationRuntimePluginSection(name: String) : Section(name) { ) } } + + /** + * Hook for adding retry classifiers to an operation's `RetryClassifiers` bundle. + * + * Should emit 1+ lines of code that look like the following: + * ```rust + * .with_classifier(AwsErrorCodeClassifier::new()) + * .with_classifier(HttpStatusCodeClassifier::new()) + * ``` + */ + data class RetryClassifier( + val configBagName: String, + val operationShape: OperationShape, + ) : OperationRuntimePluginSection("RetryClassifier") + + /** + * Hook for adding supporting types for operation-specific runtime plugins. + * Examples include various operation-specific types (retry classifiers, config bag types, etc.) + */ + data class RuntimePluginSupportingTypes( + val configBagName: String, + val operationShape: OperationShape, + ) : OperationRuntimePluginSection("RuntimePluginSupportingTypes") } typealias OperationRuntimePluginCustomization = NamedCustomization @@ -58,6 +81,7 @@ class OperationRuntimePluginGenerator( "BoxError" to runtimeApi.resolve("client::runtime_plugin::BoxError"), "ConfigBag" to runtimeApi.resolve("config_bag::ConfigBag"), "ConfigBagAccessors" to runtimeApi.resolve("client::orchestrator::ConfigBagAccessors"), + "RetryClassifiers" to runtimeApi.resolve("client::retries::RetryClassifiers"), "RuntimePlugin" to runtimeApi.resolve("client::runtime_plugin::RuntimePlugin"), ) } @@ -79,10 +103,17 @@ class OperationRuntimePluginGenerator( ${"" /* TODO(IdentityAndAuth): Resolve auth parameters from input for services that need this */} cfg.set_auth_option_resolver_params(#{AuthOptionResolverParams}::new(#{AuthOptionListResolverParams}::new())); + // Retry classifiers are operation-specific because they need to downcast operation-specific error types. + let retry_classifiers = #{RetryClassifiers}::new() + #{retry_classifier_customizations}; + cfg.set_retry_classifiers(retry_classifiers); + #{additional_config} Ok(()) } } + + #{runtime_plugin_supporting_types} """, *codegenScope, "additional_config" to writable { @@ -91,6 +122,15 @@ class OperationRuntimePluginGenerator( OperationRuntimePluginSection.AdditionalConfig("cfg", operationShape), ) }, + "retry_classifier_customizations" to writable { + writeCustomizations(customizations, OperationRuntimePluginSection.RetryClassifier("cfg", operationShape)) + }, + "runtime_plugin_supporting_types" to writable { + writeCustomizations( + customizations, + OperationRuntimePluginSection.RuntimePluginSupportingTypes("cfg", operationShape), + ) + }, ) } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt index ac5804aed9e..764efb35df2 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceGenerator.kt @@ -9,6 +9,7 @@ import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.customize.TestUtilFeature import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfigGenerator import software.amazon.smithy.rust.codegen.client.smithy.generators.error.ServiceErrorGenerator import software.amazon.smithy.rust.codegen.core.rustlang.Attribute @@ -47,6 +48,9 @@ class ServiceGenerator( serviceConfigGenerator.render(this) if (codegenContext.settings.codegenConfig.enableNewSmithyRuntime) { + // Enable users to opt in to the test-utils in the runtime crate + rustCrate.mergeFeature(TestUtilFeature.copy(deps = listOf("aws-smithy-runtime/test-util"))) + ServiceRuntimePluginGenerator(codegenContext) .render(this, decorator.serviceRuntimePluginCustomizations(codegenContext, emptyList())) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt index 8b70a847859..7f5963c572f 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt @@ -19,16 +19,6 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.Section import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations sealed class ServiceRuntimePluginSection(name: String) : Section(name) { - /** - * Hook for adding identity resolvers. - * - * Should emit code that looks like the following: - * ``` - * .identity_resolver("name", path::to::MyIdentityResolver::new()) - * ``` - */ - data class IdentityResolver(val configBagName: String) : ServiceRuntimePluginSection("IdentityResolver") - /** * Hook for adding HTTP auth schemes. * @@ -76,8 +66,8 @@ class ServiceRuntimePluginGenerator( private val endpointTypesGenerator = EndpointTypesGenerator.fromContext(codegenContext) private val codegenScope = codegenContext.runtimeConfig.let { rc -> val http = RuntimeType.smithyHttp(rc) - val runtimeApi = RuntimeType.smithyRuntimeApi(rc) val runtime = RuntimeType.smithyRuntime(rc) + val runtimeApi = RuntimeType.smithyRuntimeApi(rc) arrayOf( "AnonymousIdentityResolver" to runtimeApi.resolve("client::identity::AnonymousIdentityResolver"), "AuthOptionListResolver" to runtimeApi.resolve("client::auth::option_resolver::AuthOptionListResolver"), @@ -89,13 +79,12 @@ class ServiceRuntimePluginGenerator( "DefaultEndpointResolver" to runtime.resolve("client::orchestrator::endpoints::DefaultEndpointResolver"), "DynConnectorAdapter" to runtime.resolve("client::connections::adapter::DynConnectorAdapter"), "HttpAuthSchemes" to runtimeApi.resolve("client::orchestrator::HttpAuthSchemes"), - "IdentityResolvers" to runtimeApi.resolve("client::orchestrator::IdentityResolvers"), - "NeverRetryStrategy" to runtimeApi.resolve("client::retries::NeverRetryStrategy"), + "IdentityResolvers" to runtimeApi.resolve("client::identity::IdentityResolvers"), + "NeverRetryStrategy" to runtime.resolve("client::retries::strategy::NeverRetryStrategy"), "Params" to endpointTypesGenerator.paramsStruct(), "ResolveEndpoint" to http.resolve("endpoint::ResolveEndpoint"), "RuntimePlugin" to runtimeApi.resolve("client::runtime_plugin::RuntimePlugin"), "SharedEndpointResolver" to http.resolve("endpoint::SharedEndpointResolver"), - "TestConnection" to runtime.resolve("client::connections::test_connection::TestConnection"), "TraceProbe" to runtimeApi.resolve("client::orchestrator::TraceProbe"), ) } @@ -117,12 +106,6 @@ class ServiceRuntimePluginGenerator( fn configure(&self, cfg: &mut #{ConfigBag}) -> Result<(), #{BoxError}> { use #{ConfigBagAccessors}; - let identity_resolvers = #{IdentityResolvers}::builder() - #{identity_resolver_customizations} - .identity_resolver("anonymous", #{AnonymousIdentityResolver}::new()) - .build(); - cfg.set_identity_resolvers(identity_resolvers); - let http_auth_schemes = #{HttpAuthSchemes}::builder() #{http_auth_scheme_customizations} .build(); @@ -146,7 +129,7 @@ class ServiceRuntimePluginGenerator( let connection: Box = self.handle.conf.http_connector() .and_then(move |c| c.connector(&#{ConnectorSettings}::default(), sleep_impl)) .map(|c| Box::new(#{DynConnectorAdapter}::new(c)) as _) - .unwrap_or_else(|| Box::new(#{TestConnection}::new(vec![])) as _); + .expect("connection set"); cfg.set_connection(connection); // TODO(RuntimePlugins): Add the TraceProbe to the config bag @@ -167,9 +150,6 @@ class ServiceRuntimePluginGenerator( } """, *codegenScope, - "identity_resolver_customizations" to writable { - writeCustomizations(customizations, ServiceRuntimePluginSection.IdentityResolver("cfg")) - }, "http_auth_scheme_customizations" to writable { writeCustomizations(customizations, ServiceRuntimePluginSection.HttpAuthScheme("cfg")) }, diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/CustomizableOperationGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/CustomizableOperationGenerator.kt index ed24d439b75..a45bad43d0b 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/CustomizableOperationGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/CustomizableOperationGenerator.kt @@ -12,6 +12,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.GenericTypeArg import software.amazon.smithy.rust.codegen.core.rustlang.RustGenerics import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RustCrate @@ -20,10 +21,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.RustCrate * fluent client builders. */ class CustomizableOperationGenerator( - private val codegenContext: ClientCodegenContext, + codegenContext: ClientCodegenContext, private val generics: FluentClientGenerics, ) { - private val includeFluentClient = codegenContext.settings.codegenConfig.includeFluentClient private val runtimeConfig = codegenContext.runtimeConfig private val smithyHttp = CargoDependency.smithyHttp(runtimeConfig).toType() private val smithyTypes = CargoDependency.smithyTypes(runtimeConfig).toType() @@ -45,10 +45,6 @@ class CustomizableOperationGenerator( "RetryKind" to smithyTypes.resolve("retry::RetryKind"), ) renderCustomizableOperationModule(this) - - if (includeFluentClient) { - renderCustomizableOperationSend(this) - } } } @@ -135,26 +131,28 @@ class CustomizableOperationGenerator( *codegenScope, ) } +} - private fun renderCustomizableOperationSend(writer: RustWriter) { - val smithyHttp = CargoDependency.smithyHttp(runtimeConfig).toType() - val smithyClient = CargoDependency.smithyClient(runtimeConfig).toType() - - val operationGenerics = RustGenerics(GenericTypeArg("O"), GenericTypeArg("Retry")) - val handleGenerics = generics.toRustGenerics() - val combinedGenerics = operationGenerics + handleGenerics - - val codegenScope = arrayOf( - "combined_generics_decl" to combinedGenerics.declaration(), - "handle_generics_bounds" to handleGenerics.bounds(), - "ParseHttpResponse" to smithyHttp.resolve("response::ParseHttpResponse"), - "NewRequestPolicy" to smithyClient.resolve("retry::NewRequestPolicy"), - "SmithyRetryPolicy" to smithyClient.resolve("bounds::SmithyRetryPolicy"), - "ClassifyRetry" to RuntimeType.classifyRetry(runtimeConfig), - "SdkSuccess" to RuntimeType.sdkSuccess(runtimeConfig), - "SdkError" to RuntimeType.sdkError(runtimeConfig), - ) - +fun renderCustomizableOperationSend(runtimeConfig: RuntimeConfig, generics: FluentClientGenerics, writer: RustWriter) { + val smithyHttp = CargoDependency.smithyHttp(runtimeConfig).toType() + val smithyClient = CargoDependency.smithyClient(runtimeConfig).toType() + + val operationGenerics = RustGenerics(GenericTypeArg("O"), GenericTypeArg("Retry")) + val handleGenerics = generics.toRustGenerics() + val combinedGenerics = operationGenerics + handleGenerics + + val codegenScope = arrayOf( + "combined_generics_decl" to combinedGenerics.declaration(), + "handle_generics_bounds" to handleGenerics.bounds(), + "ParseHttpResponse" to smithyHttp.resolve("response::ParseHttpResponse"), + "NewRequestPolicy" to smithyClient.resolve("retry::NewRequestPolicy"), + "SmithyRetryPolicy" to smithyClient.resolve("bounds::SmithyRetryPolicy"), + "ClassifyRetry" to RuntimeType.classifyRetry(runtimeConfig), + "SdkSuccess" to RuntimeType.sdkSuccess(runtimeConfig), + "SdkError" to RuntimeType.sdkError(runtimeConfig), + ) + + if (generics is FlexibleClientGenerics) { writer.rustTemplate( """ impl#{combined_generics_decl:W} CustomizableOperation#{combined_generics_decl:W} @@ -176,5 +174,25 @@ class CustomizableOperationGenerator( """, *codegenScope, ) + } else { + writer.rustTemplate( + """ + impl#{combined_generics_decl:W} CustomizableOperation#{combined_generics_decl:W} + where + #{handle_generics_bounds:W} + { + /// Sends this operation's request + pub async fn send(self) -> Result> + where + E: std::error::Error + Send + Sync + 'static, + O: #{ParseHttpResponse}> + Send + Sync + Clone + 'static, + Retry: #{ClassifyRetry}<#{SdkSuccess}, #{SdkError}> + Send + Sync + Clone, + { + self.handle.client.call(self.operation).await + } + } + """, + *codegenScope, + ) } } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDecorator.kt index bc959d4a74a..e5e2760ceee 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDecorator.kt @@ -9,12 +9,14 @@ import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator import software.amazon.smithy.rust.codegen.core.rustlang.Feature import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.docs import software.amazon.smithy.rust.codegen.core.rustlang.rust 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.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.customize.NamedCustomization import software.amazon.smithy.rust.codegen.core.smithy.customize.Section @@ -34,10 +36,26 @@ class FluentClientDecorator : ClientCodegenDecorator { return } + val generics = if (codegenContext.settings.codegenConfig.enableNewSmithyRuntime) { + NoClientGenerics(codegenContext.runtimeConfig) + } else { + FlexibleClientGenerics( + connectorDefault = null, + middlewareDefault = null, + retryDefault = RuntimeType.smithyClient(codegenContext.runtimeConfig).resolve("retry::Standard"), + client = RuntimeType.smithyClient(codegenContext.runtimeConfig), + ) + } + FluentClientGenerator( codegenContext, + generics = generics, customizations = listOf(GenericFluentClient(codegenContext)), ).render(rustCrate) + rustCrate.withModule(ClientRustModule.Client.customize) { + renderCustomizableOperationSend(codegenContext.runtimeConfig, generics, this) + } + rustCrate.mergeFeature(Feature("rustls", default = true, listOf("aws-smithy-client/rustls"))) rustCrate.mergeFeature(Feature("native-tls", default = false, listOf("aws-smithy-client/native-tls"))) } 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 062e9d3d0a8..8cdc6d85629 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 @@ -55,12 +55,7 @@ import software.amazon.smithy.rust.codegen.core.util.toSnakeCase class FluentClientGenerator( private val codegenContext: ClientCodegenContext, private val reexportSmithyClientBuilder: Boolean = true, - private val generics: FluentClientGenerics = FlexibleClientGenerics( - connectorDefault = null, - middlewareDefault = null, - retryDefault = RuntimeType.smithyClient(codegenContext.runtimeConfig).resolve("retry::Standard"), - client = RuntimeType.smithyClient(codegenContext.runtimeConfig), - ), + private val generics: FluentClientGenerics, private val customizations: List = emptyList(), private val retryClassifier: RuntimeType = RuntimeType.smithyHttp(codegenContext.runtimeConfig) .resolve("retry::DefaultResponseRetryClassifier"), diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerics.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerics.kt index 399085d5e5b..d37fb89057c 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerics.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerics.kt @@ -12,8 +12,10 @@ import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +// TODO(enableNewSmithyRuntime): Delete this client generics on/off switch headache interface FluentClientGenerics { /** Declaration with defaults set */ val decl: Writable @@ -34,6 +36,37 @@ interface FluentClientGenerics { fun toRustGenerics(): RustGenerics } +class NoClientGenerics(private val runtimeConfig: RuntimeConfig) : FluentClientGenerics { + /** Declaration with defaults set */ + override val decl = writable { } + + /** Instantiation of the Smithy client generics */ + override val smithyInst = writable { + rustTemplate( + "<#{DynConnector}, #{DynMiddleware}<#{DynConnector}>>", + "DynConnector" to RuntimeType.smithyClient(runtimeConfig).resolve("erase::DynConnector"), + "DynMiddleware" to RuntimeType.smithyClient(runtimeConfig).resolve("erase::DynMiddleware"), + ) + } + + /** Instantiation */ + override val inst = "" + + /** Trait bounds */ + override val bounds = writable { } + + /** Bounds for generated `send()` functions */ + override fun sendBounds( + operation: Symbol, + operationOutput: Symbol, + operationError: Symbol, + retryClassifier: RuntimeType, + ): Writable = + writable { } + + override fun toRustGenerics() = RustGenerics() +} + data class FlexibleClientGenerics( val connectorDefault: RuntimeType?, val middlewareDefault: RuntimeType?, diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/ApiKeyAuthDecoratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ApiKeyAuthDecoratorTest.kt similarity index 98% rename from codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/ApiKeyAuthDecoratorTest.kt rename to codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ApiKeyAuthDecoratorTest.kt index b44e427777d..1673ac299f0 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/ApiKeyAuthDecoratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ApiKeyAuthDecoratorTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rust.codegen.client.customizations +package software.amazon.smithy.rust.codegen.client.smithy.customizations import org.junit.jupiter.api.Test import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecoratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecoratorTest.kt new file mode 100644 index 00000000000..29c3b2e8535 --- /dev/null +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecoratorTest.kt @@ -0,0 +1,433 @@ +/* + * 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 org.junit.jupiter.api.Test +import software.amazon.smithy.model.node.BooleanNode +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest +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.RuntimeConfig +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.integrationTest + +private fun additionalSettings(): ObjectNode = ObjectNode.objectNodeBuilder() + .withMember( + "codegen", + ObjectNode.objectNodeBuilder() + .withMember("enableNewSmithyRuntime", BooleanNode.from(true)).build(), + ) + .build() + +class HttpAuthDecoratorTest { + private fun codegenScope(runtimeConfig: RuntimeConfig): Array> = arrayOf( + "TestConnection" to CargoDependency.smithyClient(runtimeConfig) + .withFeature("test-util").toType() + .resolve("test_connection::TestConnection"), + "SdkBody" to RuntimeType.sdkBody(runtimeConfig), + ) + + @Test + fun multipleAuthSchemesSchemeSelection() { + clientIntegrationTest( + TestModels.allSchemes, + IntegrationTestParams(additionalSettings = additionalSettings()), + ) { codegenContext, rustCrate -> + rustCrate.integrationTest("tests") { + val moduleName = codegenContext.moduleUseName() + Attribute.TokioTest.render(this) + rustTemplate( + """ + async fn use_api_key_auth_when_api_key_provided() { + use aws_smithy_runtime_api::client::identity::http::Token; + + let connector = #{TestConnection}::new(vec![( + http::Request::builder() + .uri("http://localhost:1234/SomeOperation?api_key=some-api-key") + .body(#{SdkBody}::empty()) + .unwrap(), + http::Response::builder().status(200).body("").unwrap(), + )]); + + let config = $moduleName::Config::builder() + .api_key(Token::new("some-api-key", None)) + .endpoint_resolver("http://localhost:1234") + .http_connector(connector.clone()) + .build(); + let smithy_client = aws_smithy_client::Builder::new() + .connector(connector.clone()) + .middleware_fn(|r| r) + .build_dyn(); + let client = $moduleName::Client::with_config(smithy_client, config); + let _ = client.some_operation() + .send_v2() + .await + .expect("success"); + connector.assert_requests_match(&[]); + } + """, + *codegenScope(codegenContext.runtimeConfig), + ) + Attribute.TokioTest.render(this) + rustTemplate( + """ + async fn use_basic_auth_when_basic_auth_login_provided() { + use aws_smithy_runtime_api::client::identity::http::Login; + + let connector = #{TestConnection}::new(vec![( + http::Request::builder() + .header("authorization", "Basic c29tZS11c2VyOnNvbWUtcGFzcw==") + .uri("http://localhost:1234/SomeOperation") + .body(#{SdkBody}::empty()) + .unwrap(), + http::Response::builder().status(200).body("").unwrap(), + )]); + + let config = $moduleName::Config::builder() + .basic_auth_login(Login::new("some-user", "some-pass", None)) + .endpoint_resolver("http://localhost:1234") + .http_connector(connector.clone()) + .build(); + let smithy_client = aws_smithy_client::Builder::new() + .connector(connector.clone()) + .middleware_fn(|r| r) + .build_dyn(); + let client = $moduleName::Client::with_config(smithy_client, config); + let _ = client.some_operation() + .send_v2() + .await + .expect("success"); + connector.assert_requests_match(&[]); + } + """, + *codegenScope(codegenContext.runtimeConfig), + ) + } + } + } + + @Test + fun apiKeyInQueryString() { + clientIntegrationTest( + TestModels.apiKeyInQueryString, + IntegrationTestParams(additionalSettings = additionalSettings()), + ) { codegenContext, rustCrate -> + rustCrate.integrationTest("api_key_applied_to_query_string") { + val moduleName = codegenContext.moduleUseName() + Attribute.TokioTest.render(this) + rustTemplate( + """ + async fn api_key_applied_to_query_string() { + use aws_smithy_runtime_api::client::identity::http::Token; + + let connector = #{TestConnection}::new(vec![( + http::Request::builder() + .uri("http://localhost:1234/SomeOperation?api_key=some-api-key") + .body(#{SdkBody}::empty()) + .unwrap(), + http::Response::builder().status(200).body("").unwrap(), + )]); + + let config = $moduleName::Config::builder() + .api_key(Token::new("some-api-key", None)) + .endpoint_resolver("http://localhost:1234") + .http_connector(connector.clone()) + .build(); + let smithy_client = aws_smithy_client::Builder::new() + .connector(connector.clone()) + .middleware_fn(|r| r) + .build_dyn(); + let client = $moduleName::Client::with_config(smithy_client, config); + let _ = client.some_operation() + .send_v2() + .await + .expect("success"); + connector.assert_requests_match(&[]); + } + """, + *codegenScope(codegenContext.runtimeConfig), + ) + } + } + } + + @Test + fun apiKeyInHeaders() { + clientIntegrationTest( + TestModels.apiKeyInHeaders, + IntegrationTestParams(additionalSettings = additionalSettings()), + ) { codegenContext, rustCrate -> + rustCrate.integrationTest("api_key_applied_to_headers") { + val moduleName = codegenContext.moduleUseName() + Attribute.TokioTest.render(this) + rustTemplate( + """ + async fn api_key_applied_to_headers() { + use aws_smithy_runtime_api::client::identity::http::Token; + + let connector = #{TestConnection}::new(vec![( + http::Request::builder() + .header("authorization", "ApiKey some-api-key") + .uri("http://localhost:1234/SomeOperation") + .body(#{SdkBody}::empty()) + .unwrap(), + http::Response::builder().status(200).body("").unwrap(), + )]); + + let config = $moduleName::Config::builder() + .api_key(Token::new("some-api-key", None)) + .endpoint_resolver("http://localhost:1234") + .http_connector(connector.clone()) + .build(); + let smithy_client = aws_smithy_client::Builder::new() + .connector(connector.clone()) + .middleware_fn(|r| r) + .build_dyn(); + let client = $moduleName::Client::with_config(smithy_client, config); + let _ = client.some_operation() + .send_v2() + .await + .expect("success"); + connector.assert_requests_match(&[]); + } + """, + *codegenScope(codegenContext.runtimeConfig), + ) + } + } + } + + @Test + fun basicAuth() { + clientIntegrationTest( + TestModels.basicAuth, + IntegrationTestParams(additionalSettings = additionalSettings()), + ) { codegenContext, rustCrate -> + rustCrate.integrationTest("basic_auth") { + val moduleName = codegenContext.moduleUseName() + Attribute.TokioTest.render(this) + rustTemplate( + """ + async fn basic_auth() { + use aws_smithy_runtime_api::client::identity::http::Login; + + let connector = #{TestConnection}::new(vec![( + http::Request::builder() + .header("authorization", "Basic c29tZS11c2VyOnNvbWUtcGFzcw==") + .uri("http://localhost:1234/SomeOperation") + .body(#{SdkBody}::empty()) + .unwrap(), + http::Response::builder().status(200).body("").unwrap(), + )]); + + let config = $moduleName::Config::builder() + .basic_auth_login(Login::new("some-user", "some-pass", None)) + .endpoint_resolver("http://localhost:1234") + .http_connector(connector.clone()) + .build(); + let smithy_client = aws_smithy_client::Builder::new() + .connector(connector.clone()) + .middleware_fn(|r| r) + .build_dyn(); + let client = $moduleName::Client::with_config(smithy_client, config); + let _ = client.some_operation() + .send_v2() + .await + .expect("success"); + connector.assert_requests_match(&[]); + } + """, + *codegenScope(codegenContext.runtimeConfig), + ) + } + } + } + + @Test + fun bearerAuth() { + clientIntegrationTest( + TestModels.bearerAuth, + IntegrationTestParams(additionalSettings = additionalSettings()), + ) { codegenContext, rustCrate -> + rustCrate.integrationTest("bearer_auth") { + val moduleName = codegenContext.moduleUseName() + Attribute.TokioTest.render(this) + rustTemplate( + """ + async fn basic_auth() { + use aws_smithy_runtime_api::client::identity::http::Token; + + let connector = #{TestConnection}::new(vec![( + http::Request::builder() + .header("authorization", "Bearer some-token") + .uri("http://localhost:1234/SomeOperation") + .body(#{SdkBody}::empty()) + .unwrap(), + http::Response::builder().status(200).body("").unwrap(), + )]); + + let config = $moduleName::Config::builder() + .bearer_token(Token::new("some-token", None)) + .endpoint_resolver("http://localhost:1234") + .http_connector(connector.clone()) + .build(); + let smithy_client = aws_smithy_client::Builder::new() + .connector(connector.clone()) + .middleware_fn(|r| r) + .build_dyn(); + let client = $moduleName::Client::with_config(smithy_client, config); + let _ = client.some_operation() + .send_v2() + .await + .expect("success"); + connector.assert_requests_match(&[]); + } + """, + *codegenScope(codegenContext.runtimeConfig), + ) + } + } + } +} + +private object TestModels { + val allSchemes = """ + namespace test + + use aws.api#service + use aws.protocols#restJson1 + + @service(sdkId: "Test Api Key Auth") + @restJson1 + @httpApiKeyAuth(name: "api_key", in: "query") + @httpBasicAuth + @httpBearerAuth + @httpDigestAuth + @auth([httpApiKeyAuth, httpBasicAuth, httpBearerAuth, httpDigestAuth]) + service TestService { + version: "2023-01-01", + operations: [SomeOperation] + } + + structure SomeOutput { + someAttribute: Long, + someVal: String + } + + @http(uri: "/SomeOperation", method: "GET") + operation SomeOperation { + output: SomeOutput + } + """.asSmithyModel() + + val apiKeyInQueryString = """ + namespace test + + use aws.api#service + use aws.protocols#restJson1 + + @service(sdkId: "Test Api Key Auth") + @restJson1 + @httpApiKeyAuth(name: "api_key", in: "query") + @auth([httpApiKeyAuth]) + service TestService { + version: "2023-01-01", + operations: [SomeOperation] + } + + structure SomeOutput { + someAttribute: Long, + someVal: String + } + + @http(uri: "/SomeOperation", method: "GET") + operation SomeOperation { + output: SomeOutput + } + """.asSmithyModel() + + val apiKeyInHeaders = """ + namespace test + + use aws.api#service + use aws.protocols#restJson1 + + @service(sdkId: "Test Api Key Auth") + @restJson1 + @httpApiKeyAuth(name: "authorization", in: "header", scheme: "ApiKey") + @auth([httpApiKeyAuth]) + service TestService { + version: "2023-01-01", + operations: [SomeOperation] + } + + structure SomeOutput { + someAttribute: Long, + someVal: String + } + + @http(uri: "/SomeOperation", method: "GET") + operation SomeOperation { + output: SomeOutput + } + """.asSmithyModel() + + val basicAuth = """ + namespace test + + use aws.api#service + use aws.protocols#restJson1 + + @service(sdkId: "Test Api Key Auth") + @restJson1 + @httpBasicAuth + @auth([httpBasicAuth]) + service TestService { + version: "2023-01-01", + operations: [SomeOperation] + } + + structure SomeOutput { + someAttribute: Long, + someVal: String + } + + @http(uri: "/SomeOperation", method: "GET") + operation SomeOperation { + output: SomeOutput + } + """.asSmithyModel() + + val bearerAuth = """ + namespace test + + use aws.api#service + use aws.protocols#restJson1 + + @service(sdkId: "Test Api Key Auth") + @restJson1 + @httpBearerAuth + @auth([httpBearerAuth]) + service TestService { + version: "2023-01-01", + operations: [SomeOperation] + } + + structure SomeOutput { + someAttribute: Long, + someVal: String + } + + @http(uri: "/SomeOperation", method: "GET") + operation SomeOperation { + output: SomeOutput + } + """.asSmithyModel() +} diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/HttpVersionListGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpVersionListGeneratorTest.kt similarity index 99% rename from codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/HttpVersionListGeneratorTest.kt rename to codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpVersionListGeneratorTest.kt index 75366295789..3b5fc70a6f1 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/HttpVersionListGeneratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpVersionListGeneratorTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rust.codegen.client.customizations +package software.amazon.smithy.rust.codegen.client.smithy.customizations import org.junit.jupiter.api.Test import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/ResiliencyConfigCustomizationTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ResiliencyConfigCustomizationTest.kt similarity index 86% rename from codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/ResiliencyConfigCustomizationTest.kt rename to codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ResiliencyConfigCustomizationTest.kt index 612bf8e3335..6ebc6ad7599 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/customizations/ResiliencyConfigCustomizationTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/ResiliencyConfigCustomizationTest.kt @@ -3,12 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rust.codegen.client.customizations +package software.amazon.smithy.rust.codegen.client.smithy.customizations import org.junit.jupiter.api.Test import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenConfig -import software.amazon.smithy.rust.codegen.client.smithy.customizations.ResiliencyConfigCustomization -import software.amazon.smithy.rust.codegen.client.smithy.customizations.ResiliencyReExportCustomization import software.amazon.smithy.rust.codegen.client.testutil.clientRustSettings import software.amazon.smithy.rust.codegen.client.testutil.stubConfigProject import software.amazon.smithy.rust.codegen.client.testutil.testClientCodegenContext diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/ClientContextConfigCustomizationTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/ClientContextConfigCustomizationTest.kt similarity index 92% rename from codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/ClientContextConfigCustomizationTest.kt rename to codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/ClientContextConfigCustomizationTest.kt index 8c3a4122e94..32cbbddeb40 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/ClientContextConfigCustomizationTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/ClientContextConfigCustomizationTest.kt @@ -3,10 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rust.codegen.client.endpoint +package software.amazon.smithy.rust.codegen.client.smithy.endpoint import org.junit.jupiter.api.Test -import software.amazon.smithy.rust.codegen.client.smithy.endpoint.ClientContextConfigCustomization import software.amazon.smithy.rust.codegen.client.testutil.testClientCodegenContext import software.amazon.smithy.rust.codegen.client.testutil.validateConfigCustomizations import software.amazon.smithy.rust.codegen.core.rustlang.rust diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointParamsGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointParamsGeneratorTest.kt similarity index 95% rename from codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointParamsGeneratorTest.kt rename to codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointParamsGeneratorTest.kt index fedc2497945..6397e117e14 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointParamsGeneratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointParamsGeneratorTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rust.codegen.client.endpoint +package software.amazon.smithy.rust.codegen.client.smithy.endpoint import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointResolverGeneratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointResolverGeneratorTest.kt similarity index 98% rename from codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointResolverGeneratorTest.kt rename to codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointResolverGeneratorTest.kt index 179cbbc944a..b842705f994 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointResolverGeneratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointResolverGeneratorTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rust.codegen.client.endpoint +package software.amazon.smithy.rust.codegen.client.smithy.endpoint import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointsDecoratorTest.kt similarity index 99% rename from codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt rename to codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointsDecoratorTest.kt index fc564c2696d..56cd00f0ee3 100644 --- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/endpoint/EndpointsDecoratorTest.kt +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/EndpointsDecoratorTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.rust.codegen.client.endpoint +package software.amazon.smithy.rust.codegen.client.smithy.endpoint import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.string.shouldContain diff --git a/rust-runtime/aws-smithy-http-auth/src/lib.rs b/rust-runtime/aws-smithy-http-auth/src/lib.rs index b2453852537..9b9977d2ecd 100644 --- a/rust-runtime/aws-smithy-http-auth/src/lib.rs +++ b/rust-runtime/aws-smithy-http-auth/src/lib.rs @@ -3,6 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +// TODO(enableNewSmithyRuntime): The contents of this crate are moving into aws-smithy-runtime. +// This crate is kept to continue sorting the middleware implementation until it is removed. +// When removing the old implementation, clear out this crate and deprecate it. + #![allow(clippy::derive_partial_eq_without_eq)] #![warn( missing_docs, diff --git a/rust-runtime/aws-smithy-http/src/result.rs b/rust-runtime/aws-smithy-http/src/result.rs index 0ce42c7cbd3..bd6d2f1f3b8 100644 --- a/rust-runtime/aws-smithy-http/src/result.rs +++ b/rust-runtime/aws-smithy-http/src/result.rs @@ -5,15 +5,17 @@ //! `Result` wrapper types for [success](SdkSuccess) and [failure](SdkError) responses. -use crate::connection::ConnectionMetadata; -use crate::operation; -use aws_smithy_types::error::metadata::{ProvideErrorMetadata, EMPTY_ERROR_METADATA}; -use aws_smithy_types::error::ErrorMetadata; -use aws_smithy_types::retry::ErrorKind; use std::error::Error; use std::fmt; use std::fmt::{Debug, Display, Formatter}; +use aws_smithy_types::error::metadata::{ProvideErrorMetadata, EMPTY_ERROR_METADATA}; +use aws_smithy_types::error::ErrorMetadata; +use aws_smithy_types::retry::ErrorKind; + +use crate::connection::ConnectionMetadata; +use crate::operation; + type BoxError = Box; /// Successful SDK Result @@ -434,6 +436,15 @@ impl SdkError { } } + /// Return a reference to this error's raw response, if it contains one. Otherwise, return `None`. + pub fn raw_response(&self) -> Option<&R> { + match self { + Self::ServiceError(inner) => Some(inner.raw()), + Self::ResponseError(inner) => Some(inner.raw()), + _ => None, + } + } + /// Maps the service error type in `SdkError::ServiceError` #[doc(hidden)] pub fn map_service_error(self, map: impl FnOnce(E) -> E2) -> SdkError { diff --git a/rust-runtime/aws-smithy-runtime-api/Cargo.toml b/rust-runtime/aws-smithy-runtime-api/Cargo.toml index a03bac307bb..8fe513faf18 100644 --- a/rust-runtime/aws-smithy-runtime-api/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime-api/Cargo.toml @@ -9,12 +9,18 @@ repository = "https://github.com/awslabs/smithy-rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +http-auth = ["dep:zeroize"] + [dependencies] +aws-smithy-async = { path = "../aws-smithy-async" } aws-smithy-http = { path = "../aws-smithy-http" } aws-smithy-types = { path = "../aws-smithy-types" } http = "0.2.3" tokio = { version = "1.25", features = ["sync"] } tracing = "0.1" +zeroize = { version = "1", optional = true } [package.metadata.docs.rs] all-features = true diff --git a/rust-runtime/aws-smithy-runtime-api/additional-ci b/rust-runtime/aws-smithy-runtime-api/additional-ci new file mode 100755 index 00000000000..b44c6c05be7 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/additional-ci @@ -0,0 +1,12 @@ +#!/bin/bash +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# + +# This script contains additional CI checks to run for this specific package + +set -e + +echo "### Testing every combination of features (excluding --all-features)" +cargo hack test --feature-powerset --exclude-all-features diff --git a/rust-runtime/aws-smithy-runtime-api/external-types.toml b/rust-runtime/aws-smithy-runtime-api/external-types.toml index f752bef7624..ab041b9f7ec 100644 --- a/rust-runtime/aws-smithy-runtime-api/external-types.toml +++ b/rust-runtime/aws-smithy-runtime-api/external-types.toml @@ -1,4 +1,5 @@ allowed_external_types = [ + "aws_smithy_async::*", "aws_smithy_types::*", "aws_smithy_http::*", diff --git a/rust-runtime/aws-smithy-runtime-api/src/client.rs b/rust-runtime/aws-smithy-runtime-api/src/client.rs index 192b97a5d4e..1c4a97a62bd 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client.rs @@ -18,6 +18,7 @@ pub mod orchestrator; /// This code defines when and how failed requests should be retried. It also defines the behavior /// used to limit the rate that requests are sent. pub mod retries; + /// Runtime plugin type definitions. pub mod runtime_plugin; 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 278e7e04109..0438c1bdfab 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs @@ -3,4 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#[cfg(feature = "http-auth")] +pub mod http; + pub mod option_resolver; diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/auth/http.rs b/rust-runtime/aws-smithy-runtime-api/src/client/auth/http.rs new file mode 100644 index 00000000000..2f93a2eb40c --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/client/auth/http.rs @@ -0,0 +1,9 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +pub const HTTP_API_KEY_AUTH_SCHEME_ID: &str = "http-api-key-auth"; +pub const HTTP_BASIC_AUTH_SCHEME_ID: &str = "http-basic-auth"; +pub const HTTP_BEARER_AUTH_SCHEME_ID: &str = "http-bearer-auth"; +pub const HTTP_DIGEST_AUTH_SCHEME_ID: &str = "http-digest-auth"; 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 3d9c7e829dc..11a54af8202 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs @@ -3,13 +3,44 @@ * SPDX-License-Identifier: Apache-2.0 */ -use super::orchestrator::{BoxFallibleFut, IdentityResolver}; +use crate::client::orchestrator::Future; use aws_smithy_http::property_bag::PropertyBag; use std::any::Any; use std::fmt::Debug; use std::sync::Arc; use std::time::SystemTime; +#[cfg(feature = "http-auth")] +pub mod http; + +pub trait IdentityResolver: Send + Sync + Debug { + fn resolve_identity(&self, identity_properties: &PropertyBag) -> Future; +} + +#[derive(Clone, Debug, Default)] +pub struct IdentityResolvers { + identity_resolvers: Vec<(&'static str, Arc)>, +} + +impl IdentityResolvers { + pub fn builder() -> builders::IdentityResolversBuilder { + builders::IdentityResolversBuilder::new() + } + + pub fn identity_resolver(&self, identity_type: &'static str) -> Option<&dyn IdentityResolver> { + self.identity_resolvers + .iter() + .find(|resolver| resolver.0 == identity_type) + .map(|resolver| &*resolver.1) + } + + pub fn to_builder(self) -> builders::IdentityResolversBuilder { + builders::IdentityResolversBuilder { + identity_resolvers: self.identity_resolvers, + } + } +} + #[derive(Clone, Debug)] pub struct Identity { data: Arc, @@ -52,8 +83,39 @@ impl AnonymousIdentityResolver { } impl IdentityResolver for AnonymousIdentityResolver { - fn resolve_identity(&self, _: &PropertyBag) -> BoxFallibleFut { - Box::pin(async { Ok(Identity::new(AnonymousIdentity::new(), None)) }) + fn resolve_identity(&self, _: &PropertyBag) -> Future { + Future::ready(Ok(Identity::new(AnonymousIdentity::new(), None))) + } +} + +pub mod builders { + use super::*; + + #[derive(Debug, Default)] + pub struct IdentityResolversBuilder { + pub(super) identity_resolvers: Vec<(&'static str, Arc)>, + } + + impl IdentityResolversBuilder { + pub fn new() -> Self { + Default::default() + } + + pub fn identity_resolver( + mut self, + name: &'static str, + resolver: impl IdentityResolver + 'static, + ) -> Self { + self.identity_resolvers + .push((name, Arc::new(resolver) as _)); + self + } + + pub fn build(self) -> IdentityResolvers { + IdentityResolvers { + identity_resolvers: self.identity_resolvers, + } + } } } diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/identity/http.rs b/rust-runtime/aws-smithy-runtime-api/src/client/identity/http.rs new file mode 100644 index 00000000000..5a58588b025 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/client/identity/http.rs @@ -0,0 +1,130 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Identity types for HTTP auth + +use crate::client::identity::{Identity, IdentityResolver}; +use crate::client::orchestrator::Future; +use aws_smithy_http::property_bag::PropertyBag; +use std::fmt::Debug; +use std::sync::Arc; +use std::time::SystemTime; +use zeroize::Zeroizing; + +/// Identity type required to sign requests using Smithy's token-based HTTP auth schemes +/// +/// This `Token` type is used with Smithy's `@httpApiKeyAuth` and `@httpBearerAuth` +/// auth traits. +#[derive(Clone, Eq, PartialEq)] +pub struct Token(Arc); + +#[derive(Eq, PartialEq)] +struct TokenInner { + token: Zeroizing, + expiration: Option, +} + +impl Debug for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Token") + .field("token", &"** redacted **") + .finish() + } +} + +impl Token { + /// Constructs a new identity token for HTTP auth. + pub fn new(token: impl Into, expiration: Option) -> Self { + Self(Arc::new(TokenInner { + token: Zeroizing::new(token.into()), + expiration, + })) + } + + /// Returns the underlying identity token. + pub fn token(&self) -> &str { + &self.0.token + } +} + +impl From<&str> for Token { + fn from(token: &str) -> Self { + Self::from(token.to_owned()) + } +} + +impl From for Token { + fn from(api_key: String) -> Self { + Self(Arc::new(TokenInner { + token: Zeroizing::new(api_key), + expiration: None, + })) + } +} + +impl IdentityResolver for Token { + fn resolve_identity(&self, _identity_properties: &PropertyBag) -> Future { + Future::ready(Ok(Identity::new(self.clone(), self.0.expiration))) + } +} + +/// Identity type required to sign requests using Smithy's login-based HTTP auth schemes +/// +/// This `Login` type is used with Smithy's `@httpBasicAuth` and `@httpDigestAuth` +/// auth traits. +#[derive(Clone, Eq, PartialEq)] +pub struct Login(Arc); + +#[derive(Eq, PartialEq)] +struct LoginInner { + user: String, + password: Zeroizing, + expiration: Option, +} + +impl Debug for Login { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Login") + .field("user", &self.0.user) + .field("password", &"** redacted **") + .finish() + } +} + +impl Login { + /// Constructs a new identity login for HTTP auth. + pub fn new( + user: impl Into, + password: impl Into, + expiration: Option, + ) -> Self { + Self(Arc::new(LoginInner { + user: user.into(), + password: Zeroizing::new(password.into()), + expiration, + })) + } + + /// Returns the login user. + pub fn user(&self) -> &str { + &self.0.user + } + + /// Returns the login password. + pub fn password(&self) -> &str { + &self.0.password + } + + /// Returns the expiration time of this login (if any) + pub fn expiration(&self) -> Option { + self.0.expiration + } +} + +impl IdentityResolver for Login { + fn resolve_identity(&self, _identity_properties: &PropertyBag) -> Future { + Future::ready(Ok(Identity::new(self.clone(), self.0.expiration))) + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs index 35b85e3bcb7..85233f12ce2 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs @@ -3,25 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ +use super::identity::{IdentityResolver, IdentityResolvers}; use crate::client::identity::Identity; use crate::client::interceptors::context::{Input, OutputOrError}; -use crate::client::interceptors::InterceptorContext; +use crate::client::retries::RetryClassifiers; +use crate::client::retries::RetryStrategy; use crate::config_bag::ConfigBag; use crate::type_erasure::{TypeErasedBox, TypedBox}; +use aws_smithy_async::future::now_or_later::NowOrLater; use aws_smithy_http::body::SdkBody; use aws_smithy_http::endpoint::EndpointPrefix; use aws_smithy_http::property_bag::PropertyBag; use std::any::Any; use std::borrow::Cow; use std::fmt::Debug; -use std::future::Future; +use std::future::Future as StdFuture; use std::pin::Pin; use std::sync::Arc; +use std::time::SystemTime; pub type HttpRequest = http::Request; pub type HttpResponse = http::Response; pub type BoxError = Box; -pub type BoxFallibleFut = Pin>>>; +pub type BoxFuture = Pin>>>; +pub type Future = NowOrLater, BoxFuture>; pub trait TraceProbe: Send + Sync + Debug { fn dispatch_events(&self); @@ -41,25 +46,15 @@ pub trait ResponseDeserializer: Send + Sync + Debug { } pub trait Connection: Send + Sync + Debug { - fn call(&self, request: HttpRequest) -> BoxFallibleFut; + fn call(&self, request: HttpRequest) -> BoxFuture; } impl Connection for Box { - fn call(&self, request: HttpRequest) -> BoxFallibleFut { + fn call(&self, request: HttpRequest) -> BoxFuture { (**self).call(request) } } -pub trait RetryStrategy: Send + Sync + Debug { - fn should_attempt_initial_request(&self, cfg: &ConfigBag) -> Result<(), BoxError>; - - fn should_attempt_retry( - &self, - context: &InterceptorContext, - cfg: &ConfigBag, - ) -> Result; -} - #[derive(Debug)] pub struct AuthOptionResolverParams(TypeErasedBox); @@ -112,28 +107,6 @@ impl HttpAuthOption { } } -pub trait IdentityResolver: Send + Sync + Debug { - fn resolve_identity(&self, identity_properties: &PropertyBag) -> BoxFallibleFut; -} - -#[derive(Debug)] -pub struct IdentityResolvers { - identity_resolvers: Vec<(&'static str, Box)>, -} - -impl IdentityResolvers { - pub fn builder() -> builders::IdentityResolversBuilder { - builders::IdentityResolversBuilder::new() - } - - pub fn identity_resolver(&self, identity_type: &'static str) -> Option<&dyn IdentityResolver> { - self.identity_resolvers - .iter() - .find(|resolver| resolver.0 == identity_type) - .map(|resolver| &*resolver.1) - } -} - #[derive(Debug)] struct HttpAuthSchemesInner { schemes: Vec<(&'static str, Box)>, @@ -202,6 +175,29 @@ pub trait EndpointResolver: Send + Sync + Debug { ) -> Result<(), BoxError>; } +/// Time that the request is being made (so that time can be overridden in the [`ConfigBag`]). +#[non_exhaustive] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct RequestTime(SystemTime); + +impl Default for RequestTime { + fn default() -> Self { + Self(SystemTime::now()) + } +} + +impl RequestTime { + /// Create a new [`RequestTime`]. + pub fn new(time: SystemTime) -> Self { + Self(time) + } + + /// Returns the request time as a [`SystemTime`]. + pub fn system_time(&self) -> SystemTime { + self.0 + } +} + pub trait ConfigBagAccessors { fn auth_option_resolver_params(&self) -> &AuthOptionResolverParams; fn set_auth_option_resolver_params( @@ -236,11 +232,17 @@ pub trait ConfigBagAccessors { response_serializer: impl ResponseDeserializer + 'static, ); + fn retry_classifiers(&self) -> &RetryClassifiers; + fn set_retry_classifiers(&mut self, retry_classifier: RetryClassifiers); + fn retry_strategy(&self) -> &dyn RetryStrategy; fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static); fn trace_probe(&self) -> &dyn TraceProbe; fn set_trace_probe(&mut self, trace_probe: impl TraceProbe + 'static); + + fn request_time(&self) -> Option; + fn set_request_time(&mut self, request_time: RequestTime); } impl ConfigBagAccessors for ConfigBag { @@ -269,25 +271,6 @@ impl ConfigBagAccessors for ConfigBag { self.put::>(Box::new(auth_option_resolver)); } - fn http_auth_schemes(&self) -> &HttpAuthSchemes { - self.get::() - .expect("auth schemes must be set") - } - - fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes) { - self.put::(http_auth_schemes); - } - - fn retry_strategy(&self) -> &dyn RetryStrategy { - &**self - .get::>() - .expect("a retry strategy must be set") - } - - fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static) { - self.put::>(Box::new(retry_strategy)); - } - fn endpoint_resolver_params(&self) -> &EndpointResolverParams { self.get::() .expect("endpoint resolver params must be set") @@ -326,6 +309,15 @@ impl ConfigBagAccessors for ConfigBag { self.put::>(Box::new(connection)); } + fn http_auth_schemes(&self) -> &HttpAuthSchemes { + self.get::() + .expect("auth schemes must be set") + } + + fn set_http_auth_schemes(&mut self, http_auth_schemes: HttpAuthSchemes) { + self.put::(http_auth_schemes); + } + fn request_serializer(&self) -> &dyn RequestSerializer { &**self .get::>() @@ -349,6 +341,25 @@ impl ConfigBagAccessors for ConfigBag { self.put::>(Box::new(response_deserializer)); } + fn retry_classifiers(&self) -> &RetryClassifiers { + self.get::() + .expect("retry classifiers must be set") + } + + fn set_retry_classifiers(&mut self, retry_classifiers: RetryClassifiers) { + self.put::(retry_classifiers); + } + + fn retry_strategy(&self) -> &dyn RetryStrategy { + &**self + .get::>() + .expect("a retry strategy must be set") + } + + fn set_retry_strategy(&mut self, retry_strategy: impl RetryStrategy + 'static) { + self.put::>(Box::new(retry_strategy)); + } + fn trace_probe(&self) -> &dyn TraceProbe { &**self .get::>() @@ -358,37 +369,18 @@ impl ConfigBagAccessors for ConfigBag { fn set_trace_probe(&mut self, trace_probe: impl TraceProbe + 'static) { self.put::>(Box::new(trace_probe)); } -} - -pub mod builders { - use super::*; - #[derive(Debug, Default)] - pub struct IdentityResolversBuilder { - identity_resolvers: Vec<(&'static str, Box)>, + fn request_time(&self) -> Option { + self.get::().cloned() } - impl IdentityResolversBuilder { - pub fn new() -> Self { - Default::default() - } - - pub fn identity_resolver( - mut self, - name: &'static str, - resolver: impl IdentityResolver + 'static, - ) -> Self { - self.identity_resolvers - .push((name, Box::new(resolver) as _)); - self - } - - pub fn build(self) -> IdentityResolvers { - IdentityResolvers { - identity_resolvers: self.identity_resolvers, - } - } + fn set_request_time(&mut self, request_time: RequestTime) { + self.put::(request_time); } +} + +pub mod builders { + use super::*; #[derive(Debug, Default)] pub struct HttpAuthSchemesBuilder { 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 fd840a7bc9c..a53f1d242c2 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs @@ -3,32 +3,72 @@ * SPDX-License-Identifier: Apache-2.0 */ +use crate::client::interceptors::context::Error; use crate::client::interceptors::InterceptorContext; -use crate::client::orchestrator::{BoxError, RetryStrategy}; +use crate::client::orchestrator::{BoxError, HttpRequest, HttpResponse}; use crate::config_bag::ConfigBag; -use aws_smithy_http::body::SdkBody; +use aws_smithy_types::retry::ErrorKind; +use std::fmt::Debug; +use std::time::Duration; -pub mod rate_limiting; +/// An answer to the question "should I make a request attempt?" +pub enum ShouldAttempt { + Yes, + No, + YesAfterDelay(Duration), +} + +pub trait RetryStrategy: Send + Sync + Debug { + fn should_attempt_initial_request(&self, cfg: &ConfigBag) -> Result; -#[derive(Debug, Clone)] -pub struct NeverRetryStrategy {} + fn should_attempt_retry( + &self, + context: &InterceptorContext, + cfg: &ConfigBag, + ) -> Result; +} + +#[non_exhaustive] +#[derive(Eq, PartialEq, Debug)] +pub enum RetryReason { + Error(ErrorKind), + Explicit(Duration), +} + +/// Classifies what kind of retry is needed for a given [`Error`]. +pub trait ClassifyRetry: Send + Sync + Debug { + /// Run this classifier against an error to determine if it should be retried. Returns + /// `Some(RetryKind)` if the error should be retried; Otherwise returns `None`. + fn classify_retry(&self, error: &Error) -> Option; +} -impl NeverRetryStrategy { +#[derive(Debug)] +pub struct RetryClassifiers { + inner: Vec>, +} + +impl RetryClassifiers { pub fn new() -> Self { - Self {} + Self { + // It's always expected that at least one classifier will be defined, + // so we eagerly allocate for it. + inner: Vec::with_capacity(1), + } } -} -impl RetryStrategy for NeverRetryStrategy { - fn should_attempt_initial_request(&self, _cfg: &ConfigBag) -> Result<(), BoxError> { - Ok(()) + pub fn with_classifier(mut self, retry_classifier: impl ClassifyRetry + 'static) -> Self { + self.inner.push(Box::new(retry_classifier)); + + self } - fn should_attempt_retry( - &self, - _context: &InterceptorContext, http::Response>, - _cfg: &ConfigBag, - ) -> Result { - Ok(false) + // TODO(https://github.com/awslabs/smithy-rs/issues/2632) make a map function so users can front-run or second-guess the classifier's decision + // pub fn map_classifiers(mut self, fun: Fn() -> RetryClassifiers) +} + +impl ClassifyRetry for RetryClassifiers { + fn classify_retry(&self, error: &Error) -> Option { + // return the first non-None result + self.inner.iter().find_map(|cr| cr.classify_retry(error)) } } diff --git a/rust-runtime/aws-smithy-runtime/Cargo.toml b/rust-runtime/aws-smithy-runtime/Cargo.toml index 4ff7e7613d1..5c8e098462d 100644 --- a/rust-runtime/aws-smithy-runtime/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/awslabs/smithy-rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] +http-auth = ["aws-smithy-runtime-api/http-auth"] test-util = ["dep:aws-smithy-protocol-test"] [dependencies] @@ -25,6 +26,9 @@ pin-utils = "0.1.0" tokio = { version = "1.25", features = [] } tracing = "0.1" +[dev-dependencies] +tokio = { version = "1.25", features = ["macros", "rt"] } + [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu"] diff --git a/rust-runtime/aws-smithy-runtime/external-types.toml b/rust-runtime/aws-smithy-runtime/external-types.toml index a8a47f10d99..92360e722af 100644 --- a/rust-runtime/aws-smithy-runtime/external-types.toml +++ b/rust-runtime/aws-smithy-runtime/external-types.toml @@ -1,6 +1,7 @@ allowed_external_types = [ "aws_smithy_runtime_api::*", "aws_smithy_http::*", + "aws_smithy_types::*", "aws_smithy_client::erase::DynConnector", # TODO(audit-external-type-usage) We should newtype these or otherwise avoid exposing them "http::header::name::HeaderName", diff --git a/rust-runtime/aws-smithy-runtime/src/client.rs b/rust-runtime/aws-smithy-runtime/src/client.rs index e41bda8ed2f..d477ce9eaec 100644 --- a/rust-runtime/aws-smithy-runtime/src/client.rs +++ b/rust-runtime/aws-smithy-runtime/src/client.rs @@ -3,7 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ +pub mod auth; + pub mod orchestrator; /// Smithy connector runtime plugins pub mod connections; + +/// Smithy code related to retry handling and token buckets. +/// +/// This code defines when and how failed requests should be retried. It also defines the behavior +/// used to limit the rate at which requests are sent. +pub mod retries; diff --git a/rust-runtime/aws-smithy-runtime/src/client/auth.rs b/rust-runtime/aws-smithy-runtime/src/client/auth.rs new file mode 100644 index 00000000000..d06c1e4e86d --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/auth.rs @@ -0,0 +1,7 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#[cfg(feature = "http-auth")] +pub mod http; diff --git a/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs b/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs new file mode 100644 index 00000000000..8bacee21f0e --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs @@ -0,0 +1,348 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_http::property_bag::PropertyBag; +use aws_smithy_http::query_writer::QueryWriter; +use aws_smithy_runtime_api::client::auth::http::{ + HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID, + HTTP_DIGEST_AUTH_SCHEME_ID, +}; +use aws_smithy_runtime_api::client::identity::http::{Login, Token}; +use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver, IdentityResolvers}; +use aws_smithy_runtime_api::client::orchestrator::{ + BoxError, HttpAuthScheme, HttpRequest, HttpRequestSigner, +}; +use aws_smithy_types::base64::encode; +use http::header::HeaderName; +use http::HeaderValue; + +/// Destination for the API key +#[derive(Copy, Clone, Debug)] +pub enum ApiKeyLocation { + Query, + Header, +} + +/// Auth implementation for Smithy's `@httpApiKey` auth scheme +#[derive(Debug)] +pub struct ApiKeyAuthScheme { + signer: ApiKeySigner, +} + +impl ApiKeyAuthScheme { + /// Creates a new `ApiKeyAuthScheme`. + pub fn new( + scheme: impl Into, + location: ApiKeyLocation, + name: impl Into, + ) -> Self { + Self { + signer: ApiKeySigner { + scheme: scheme.into(), + location, + name: name.into(), + }, + } + } +} + +impl HttpAuthScheme for ApiKeyAuthScheme { + fn scheme_id(&self) -> &'static str { + HTTP_API_KEY_AUTH_SCHEME_ID + } + + fn identity_resolver<'a>( + &self, + identity_resolvers: &'a IdentityResolvers, + ) -> Option<&'a dyn IdentityResolver> { + identity_resolvers.identity_resolver(self.scheme_id()) + } + + fn request_signer(&self) -> &dyn HttpRequestSigner { + &self.signer + } +} + +#[derive(Debug)] +struct ApiKeySigner { + scheme: String, + location: ApiKeyLocation, + name: String, +} + +impl HttpRequestSigner for ApiKeySigner { + fn sign_request( + &self, + request: &mut HttpRequest, + identity: &Identity, + _signing_properties: &PropertyBag, + ) -> Result<(), BoxError> { + let api_key = identity + .data::() + .ok_or("HTTP ApiKey auth requires a `Token` identity")?; + match self.location { + ApiKeyLocation::Header => { + request.headers_mut().append( + HeaderName::try_from(&self.name).expect("valid API key header name"), + HeaderValue::try_from(format!("{} {}", self.scheme, api_key.token())).map_err( + |_| "API key contains characters that can't be included in a HTTP header", + )?, + ); + } + ApiKeyLocation::Query => { + let mut query = QueryWriter::new(request.uri()); + query.insert(&self.name, api_key.token()); + *request.uri_mut() = query.build_uri(); + } + } + + Ok(()) + } +} + +/// Auth implementation for Smithy's `@httpBasicAuth` auth scheme +#[derive(Debug, Default)] +pub struct BasicAuthScheme { + signer: BasicAuthSigner, +} + +impl BasicAuthScheme { + /// Creates a new `BasicAuthScheme`. + pub fn new() -> Self { + Self { + signer: BasicAuthSigner, + } + } +} + +impl HttpAuthScheme for BasicAuthScheme { + fn scheme_id(&self) -> &'static str { + HTTP_BASIC_AUTH_SCHEME_ID + } + + fn identity_resolver<'a>( + &self, + identity_resolvers: &'a IdentityResolvers, + ) -> Option<&'a dyn IdentityResolver> { + identity_resolvers.identity_resolver(self.scheme_id()) + } + + fn request_signer(&self) -> &dyn HttpRequestSigner { + &self.signer + } +} + +#[derive(Debug, Default)] +struct BasicAuthSigner; + +impl HttpRequestSigner for BasicAuthSigner { + fn sign_request( + &self, + request: &mut HttpRequest, + identity: &Identity, + _signing_properties: &PropertyBag, + ) -> Result<(), BoxError> { + let login = identity + .data::() + .ok_or("HTTP basic auth requires a `Login` identity")?; + request.headers_mut().insert( + http::header::AUTHORIZATION, + HeaderValue::from_str(&format!( + "Basic {}", + encode(format!("{}:{}", login.user(), login.password())) + )) + .expect("valid header value"), + ); + Ok(()) + } +} + +/// Auth implementation for Smithy's `@httpBearerAuth` auth scheme +#[derive(Debug, Default)] +pub struct BearerAuthScheme { + signer: BearerAuthSigner, +} + +impl BearerAuthScheme { + /// Creates a new `BearerAuthScheme`. + pub fn new() -> Self { + Self { + signer: BearerAuthSigner, + } + } +} + +impl HttpAuthScheme for BearerAuthScheme { + fn scheme_id(&self) -> &'static str { + HTTP_BEARER_AUTH_SCHEME_ID + } + + fn identity_resolver<'a>( + &self, + identity_resolvers: &'a IdentityResolvers, + ) -> Option<&'a dyn IdentityResolver> { + identity_resolvers.identity_resolver(self.scheme_id()) + } + + fn request_signer(&self) -> &dyn HttpRequestSigner { + &self.signer + } +} + +#[derive(Debug, Default)] +struct BearerAuthSigner; + +impl HttpRequestSigner for BearerAuthSigner { + fn sign_request( + &self, + request: &mut HttpRequest, + identity: &Identity, + _signing_properties: &PropertyBag, + ) -> Result<(), BoxError> { + let token = identity + .data::() + .ok_or("HTTP bearer auth requires a `Token` identity")?; + request.headers_mut().insert( + http::header::AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", token.token())).map_err(|_| { + "Bearer token contains characters that can't be included in a HTTP header" + })?, + ); + Ok(()) + } +} + +/// Auth implementation for Smithy's `@httpDigestAuth` auth scheme +#[derive(Debug, Default)] +pub struct DigestAuthScheme { + signer: DigestAuthSigner, +} + +impl DigestAuthScheme { + /// Creates a new `DigestAuthScheme`. + pub fn new() -> Self { + Self { + signer: DigestAuthSigner, + } + } +} + +impl HttpAuthScheme for DigestAuthScheme { + fn scheme_id(&self) -> &'static str { + HTTP_DIGEST_AUTH_SCHEME_ID + } + + fn identity_resolver<'a>( + &self, + identity_resolvers: &'a IdentityResolvers, + ) -> Option<&'a dyn IdentityResolver> { + identity_resolvers.identity_resolver(self.scheme_id()) + } + + fn request_signer(&self) -> &dyn HttpRequestSigner { + &self.signer + } +} + +#[derive(Debug, Default)] +struct DigestAuthSigner; + +impl HttpRequestSigner for DigestAuthSigner { + fn sign_request( + &self, + _request: &mut HttpRequest, + _identity: &Identity, + _signing_properties: &PropertyBag, + ) -> Result<(), BoxError> { + unimplemented!( + "support for signing with Smithy's `@httpDigestAuth` auth scheme is not implemented yet" + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_smithy_http::body::SdkBody; + use aws_smithy_runtime_api::client::identity::http::Login; + + #[test] + fn test_api_key_signing_headers() { + let signer = ApiKeySigner { + scheme: "SomeSchemeName".into(), + location: ApiKeyLocation::Header, + name: "some-header-name".into(), + }; + let signing_properties = PropertyBag::new(); + let identity = Identity::new(Token::new("some-token", None), None); + let mut request = http::Request::builder() + .uri("http://example.com/Foobaz") + .body(SdkBody::empty()) + .unwrap(); + signer + .sign_request(&mut request, &identity, &signing_properties) + .expect("success"); + assert_eq!( + "SomeSchemeName some-token", + request.headers().get("some-header-name").unwrap() + ); + assert_eq!("http://example.com/Foobaz", request.uri().to_string()); + } + + #[test] + fn test_api_key_signing_query() { + let signer = ApiKeySigner { + scheme: "".into(), + location: ApiKeyLocation::Query, + name: "some-query-name".into(), + }; + let signing_properties = PropertyBag::new(); + let identity = Identity::new(Token::new("some-token", None), None); + let mut request = http::Request::builder() + .uri("http://example.com/Foobaz") + .body(SdkBody::empty()) + .unwrap(); + signer + .sign_request(&mut request, &identity, &signing_properties) + .expect("success"); + assert!(request.headers().get("some-query-name").is_none()); + assert_eq!( + "http://example.com/Foobaz?some-query-name=some-token", + request.uri().to_string() + ); + } + + #[test] + fn test_basic_auth() { + let signer = BasicAuthSigner; + let signing_properties = PropertyBag::new(); + let identity = Identity::new(Login::new("Aladdin", "open sesame", None), None); + let mut request = http::Request::builder().body(SdkBody::empty()).unwrap(); + + signer + .sign_request(&mut request, &identity, &signing_properties) + .expect("success"); + assert_eq!( + "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + request.headers().get("Authorization").unwrap() + ); + } + + #[test] + fn test_bearer_auth() { + let signer = BearerAuthSigner; + + let signing_properties = PropertyBag::new(); + let identity = Identity::new(Token::new("some-token", None), None); + let mut request = http::Request::builder().body(SdkBody::empty()).unwrap(); + signer + .sign_request(&mut request, &identity, &signing_properties) + .expect("success"); + assert_eq!( + "Bearer some-token", + request.headers().get("Authorization").unwrap() + ); + } +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/connections.rs b/rust-runtime/aws-smithy-runtime/src/client/connections.rs index d1d8b55658b..e05eedf2e80 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/connections.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/connections.rs @@ -9,7 +9,7 @@ pub mod test_connection; pub mod adapter { use aws_smithy_client::erase::DynConnector; use aws_smithy_runtime_api::client::orchestrator::{ - BoxFallibleFut, Connection, HttpRequest, HttpResponse, + BoxFuture, Connection, HttpRequest, HttpResponse, }; use std::sync::{Arc, Mutex}; @@ -28,7 +28,7 @@ pub mod adapter { } impl Connection for DynConnectorAdapter { - fn call(&self, request: HttpRequest) -> BoxFallibleFut { + fn call(&self, request: HttpRequest) -> BoxFuture { let future = self.dyn_connector.lock().unwrap().call_lite(request); future } diff --git a/rust-runtime/aws-smithy-runtime/src/client/connections/test_connection.rs b/rust-runtime/aws-smithy-runtime/src/client/connections/test_connection.rs index 7366615ab86..f948daf7460 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/connections/test_connection.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/connections/test_connection.rs @@ -9,7 +9,7 @@ use aws_smithy_http::body::SdkBody; use aws_smithy_http::result::ConnectorError; use aws_smithy_protocol_test::{assert_ok, validate_body, MediaType}; use aws_smithy_runtime_api::client::orchestrator::{ - BoxFallibleFut, Connection, HttpRequest, HttpResponse, + BoxFuture, Connection, HttpRequest, HttpResponse, }; use http::header::{HeaderName, CONTENT_TYPE}; use std::fmt::Debug; @@ -187,7 +187,7 @@ impl TestConnection { } impl Connection for TestConnection { - fn call(&self, request: HttpRequest) -> BoxFallibleFut { + fn call(&self, request: HttpRequest) -> BoxFuture { // TODO(orchestrator) Validate request let res = if let Some((expected, resp)) = self.data.lock().unwrap().pop() { diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs index dff37ec7b0b..adba3dfb045 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs @@ -13,6 +13,7 @@ use aws_smithy_runtime_api::client::interceptors::{InterceptorContext, Intercept use aws_smithy_runtime_api::client::orchestrator::{ BoxError, ConfigBagAccessors, HttpRequest, HttpResponse, }; +use aws_smithy_runtime_api::client::retries::ShouldAttempt; use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugins; use aws_smithy_runtime_api::config_bag::ConfigBag; use tracing::{debug_span, Instrument}; @@ -61,9 +62,18 @@ pub async fn invoke( let retry_strategy = cfg.retry_strategy(); match retry_strategy.should_attempt_initial_request(cfg) { // Yes, let's make a request - Ok(_) => {} + Ok(ShouldAttempt::Yes) => {} + // No, this request shouldn't be sent + Ok(ShouldAttempt::No) => { + return Err(Phase::dispatch(context).fail( + "The retry strategy indicates that an initial request shouldn't be made, but it didn't specify why.", + )) + } // No, we shouldn't make a request because... Err(err) => return Err(Phase::dispatch(context).fail(err)), + Ok(ShouldAttempt::YesAfterDelay(_)) => { + unreachable!("Delaying the initial request is currently unsupported. If this feature is important to you, please file an issue in GitHub.") + } } } @@ -79,9 +89,12 @@ pub async fn invoke( let retry_strategy = cfg.retry_strategy(); match retry_strategy.should_attempt_retry(&context, cfg) { // Yes, let's retry the request - Ok(true) => continue, + Ok(ShouldAttempt::Yes) => continue, // No, this request shouldn't be retried - Ok(false) => {} + Ok(ShouldAttempt::No) => {} + Ok(ShouldAttempt::YesAfterDelay(_delay)) => { + todo!("implement retries with an explicit delay.") + } // I couldn't determine if the request should be retried because an error occurred. Err(err) => { return Err(Phase::response_handling(context).fail(err)); 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 4a990019205..77c6b6f21da 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs @@ -24,29 +24,29 @@ pub(super) async fn orchestrate_auth( .map_err(construction_failure)?; let identity_resolvers = cfg.identity_resolvers(); + tracing::trace!( + auth_option_resolver_params = ?params, + auth_options = ?auth_options, + identity_resolvers = ?identity_resolvers, + "orchestrating auth", + ); for option in auth_options.as_ref() { let scheme_id = option.scheme_id(); let scheme_properties = option.properties(); if let Some(auth_scheme) = cfg.http_auth_schemes().scheme(scheme_id) { - let identity_resolver = auth_scheme - .identity_resolver(identity_resolvers) - .ok_or_else(|| { - construction_failure(format!( - "no identity resolver found for auth scheme {id}. This is a bug. Please file an issue.", - id = auth_scheme.scheme_id() - )) - })?; - let request_signer = auth_scheme.request_signer(); - - let identity = identity_resolver - .resolve_identity(scheme_properties) - .await - .map_err(construction_failure)?; - return dispatch_phase.include_mut(|ctx| { - let request = ctx.request_mut()?; - request_signer.sign_request(request, &identity, scheme_properties)?; - Result::<_, BoxError>::Ok(()) - }); + if let Some(identity_resolver) = auth_scheme.identity_resolver(identity_resolvers) { + let request_signer = auth_scheme.request_signer(); + + let identity = identity_resolver + .resolve_identity(scheme_properties) + .await + .map_err(construction_failure)?; + return dispatch_phase.include_mut(|ctx| { + let request = ctx.request_mut()?; + request_signer.sign_request(request, &identity, scheme_properties)?; + Result::<_, BoxError>::Ok(()) + }); + } } } @@ -54,3 +54,180 @@ pub(super) async fn orchestrate_auth( "no auth scheme matched auth options. This is a bug. Please file an issue.", )) } + +#[cfg(test)] +mod tests { + use super::*; + use aws_smithy_http::body::SdkBody; + use aws_smithy_http::property_bag::PropertyBag; + use aws_smithy_runtime_api::client::auth::option_resolver::AuthOptionListResolver; + use aws_smithy_runtime_api::client::identity::{Identity, IdentityResolver, IdentityResolvers}; + use aws_smithy_runtime_api::client::interceptors::InterceptorContext; + use aws_smithy_runtime_api::client::orchestrator::{ + AuthOptionResolverParams, Future, HttpAuthOption, HttpAuthScheme, HttpAuthSchemes, + HttpRequest, HttpRequestSigner, + }; + use aws_smithy_runtime_api::type_erasure::TypedBox; + use std::sync::Arc; + + #[tokio::test] + async fn basic_case() { + #[derive(Debug)] + struct TestIdentityResolver; + impl IdentityResolver for TestIdentityResolver { + fn resolve_identity(&self, _identity_properties: &PropertyBag) -> Future { + Future::ready(Ok(Identity::new("doesntmatter", None))) + } + } + + #[derive(Debug)] + struct TestSigner; + + impl HttpRequestSigner for TestSigner { + fn sign_request( + &self, + request: &mut HttpRequest, + _identity: &Identity, + _signing_properties: &PropertyBag, + ) -> Result<(), BoxError> { + request + .headers_mut() + .insert(http::header::AUTHORIZATION, "success!".parse().unwrap()); + Ok(()) + } + } + + #[derive(Debug)] + struct TestAuthScheme { + signer: TestSigner, + } + impl HttpAuthScheme for TestAuthScheme { + fn scheme_id(&self) -> &'static str { + "test-scheme" + } + + fn identity_resolver<'a>( + &self, + identity_resolvers: &'a IdentityResolvers, + ) -> Option<&'a dyn IdentityResolver> { + identity_resolvers.identity_resolver(self.scheme_id()) + } + + fn request_signer(&self) -> &dyn HttpRequestSigner { + &self.signer + } + } + + let input = TypedBox::new("doesnt-matter").erase(); + let mut context = InterceptorContext::new(input); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let mut cfg = ConfigBag::base(); + cfg.set_auth_option_resolver_params(AuthOptionResolverParams::new("doesntmatter")); + cfg.set_auth_option_resolver(AuthOptionListResolver::new(vec![HttpAuthOption::new( + "test-scheme", + Arc::new(PropertyBag::new()), + )])); + cfg.set_identity_resolvers( + IdentityResolvers::builder() + .identity_resolver("test-scheme", TestIdentityResolver) + .build(), + ); + cfg.set_http_auth_schemes( + HttpAuthSchemes::builder() + .auth_scheme("test-scheme", TestAuthScheme { signer: TestSigner }) + .build(), + ); + + let phase = Phase::dispatch(context); + let context = orchestrate_auth(phase, &cfg) + .await + .expect("success") + .finish(); + + assert_eq!( + "success!", + context + .request() + .unwrap() + .headers() + .get("Authorization") + .unwrap() + ); + } + + #[cfg(feature = "http-auth")] + #[tokio::test] + async fn select_best_scheme_for_available_identity_resolvers() { + use crate::client::auth::http::{BasicAuthScheme, BearerAuthScheme}; + use aws_smithy_runtime_api::client::auth::http::{ + HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID, + }; + use aws_smithy_runtime_api::client::identity::http::{Login, Token}; + + let mut context = InterceptorContext::new(TypedBox::new("doesnt-matter").erase()); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let mut cfg = ConfigBag::base(); + cfg.set_auth_option_resolver_params(AuthOptionResolverParams::new("doesntmatter")); + cfg.set_auth_option_resolver(AuthOptionListResolver::new(vec![ + HttpAuthOption::new(HTTP_BASIC_AUTH_SCHEME_ID, Arc::new(PropertyBag::new())), + HttpAuthOption::new(HTTP_BEARER_AUTH_SCHEME_ID, Arc::new(PropertyBag::new())), + ])); + cfg.set_http_auth_schemes( + HttpAuthSchemes::builder() + .auth_scheme(HTTP_BASIC_AUTH_SCHEME_ID, BasicAuthScheme::new()) + .auth_scheme(HTTP_BEARER_AUTH_SCHEME_ID, BearerAuthScheme::new()) + .build(), + ); + + // First, test the presence of a basic auth login and absence of a bearer token + cfg.set_identity_resolvers( + IdentityResolvers::builder() + .identity_resolver(HTTP_BASIC_AUTH_SCHEME_ID, Login::new("a", "b", None)) + .build(), + ); + + let phase = Phase::dispatch(context); + let context = orchestrate_auth(phase, &cfg) + .await + .expect("success") + .finish(); + + assert_eq!( + // "YTpi" == "a:b" in base64 + "Basic YTpi", + context + .request() + .unwrap() + .headers() + .get("Authorization") + .unwrap() + ); + + // Next, test the presence of a bearer token and absence of basic auth + cfg.set_identity_resolvers( + IdentityResolvers::builder() + .identity_resolver(HTTP_BEARER_AUTH_SCHEME_ID, Token::new("t", None)) + .build(), + ); + + let mut context = InterceptorContext::new(TypedBox::new("doesnt-matter").erase()); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let context = orchestrate_auth(Phase::dispatch(context), &cfg) + .await + .expect("success") + .finish(); + + assert_eq!( + "Bearer t", + context + .request() + .unwrap() + .headers() + .get("Authorization") + .unwrap() + ); + } +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries.rs b/rust-runtime/aws-smithy-runtime/src/client/retries.rs new file mode 100644 index 00000000000..f8bbe70060a --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries.rs @@ -0,0 +1,7 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +pub mod classifier; +pub mod strategy; diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries/classifier.rs b/rust-runtime/aws-smithy-runtime/src/client/retries/classifier.rs new file mode 100644 index 00000000000..64960707ad1 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries/classifier.rs @@ -0,0 +1,195 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_http::result::SdkError; +use aws_smithy_runtime_api::client::retries::RetryReason; +use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind}; +use std::borrow::Cow; + +/// A retry classifier for checking if an error is modeled as retryable. +#[derive(Debug)] +pub struct ModeledAsRetryableClassifier; + +impl ModeledAsRetryableClassifier { + /// Check if an error is modeled as retryable, returning a [`RetryReason::Error`] if it is. + pub fn classify_error( + &self, + error: &SdkError, + ) -> Option { + match error { + SdkError::ServiceError(inner) => { + inner.err().retryable_error_kind().map(RetryReason::Error) + } + _ => None, + } + } +} + +#[derive(Debug)] +pub struct SmithyErrorClassifier; + +impl SmithyErrorClassifier { + pub fn classify_error(&self, result: &SdkError) -> Option { + match result { + SdkError::TimeoutError(_err) => Some(RetryReason::Error(ErrorKind::TransientError)), + SdkError::ResponseError { .. } => Some(RetryReason::Error(ErrorKind::TransientError)), + SdkError::DispatchFailure(err) if (err.is_timeout() || err.is_io()) => { + Some(RetryReason::Error(ErrorKind::TransientError)) + } + SdkError::DispatchFailure(err) => err.is_other().map(RetryReason::Error), + _ => None, + } + } +} + +const TRANSIENT_ERROR_STATUS_CODES: &[u16] = &[500, 502, 503, 504]; + +/// A retry classifier that will treat HTTP response with those status codes as retryable. +/// The `Default` version will retry 500, 502, 503, and 504 errors. +#[derive(Debug)] +pub struct HttpStatusCodeClassifier { + retryable_status_codes: Cow<'static, [u16]>, +} + +impl HttpStatusCodeClassifier { + /// Given a `Vec` where the `u16`s represent status codes, create a retry classifier that will + /// treat HTTP response with those status codes as retryable. The `Default` version will retry + /// 500, 502, 503, and 504 errors. + pub fn new_from_codes(retryable_status_codes: impl Into>) -> Self { + Self { + retryable_status_codes: retryable_status_codes.into(), + } + } + + /// Classify an HTTP response based on its status code. + pub fn classify_error(&self, error: &SdkError) -> Option { + error + .raw_response() + .map(|res| res.http().status().as_u16()) + .map(|status| self.retryable_status_codes.contains(&status)) + .unwrap_or_default() + .then_some(RetryReason::Error(ErrorKind::TransientError)) + } +} + +impl Default for HttpStatusCodeClassifier { + fn default() -> Self { + Self::new_from_codes(TRANSIENT_ERROR_STATUS_CODES.to_owned()) + } +} + +// Generic smithy clients would have something like this: +// pub fn default_retry_classifiers() -> RetryClassifiers { +// RetryClassifiers::new() +// .with_classifier(SmithyErrorClassifier::new()) +// .with_classifier(ModeledAsRetryableClassifier::new()) +// .with_classifier(HttpStatusCodeClassifier::new()) +// } +// This ordering is different than the default AWS ordering because the old generic client classifer +// was the same. + +#[cfg(test)] +mod test { + use std::fmt; + + use crate::client::retries::classifier::{ + HttpStatusCodeClassifier, ModeledAsRetryableClassifier, + }; + use aws_smithy_http::body::SdkBody; + use aws_smithy_http::operation; + use aws_smithy_http::result::SdkError; + use aws_smithy_runtime_api::client::retries::RetryReason; + use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind}; + + use super::SmithyErrorClassifier; + + #[derive(Debug)] + struct UnmodeledError; + + impl fmt::Display for UnmodeledError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UnmodeledError") + } + } + + impl std::error::Error for UnmodeledError {} + + #[test] + fn classify_by_response_status() { + let policy = HttpStatusCodeClassifier::default(); + let res = http::Response::builder() + .status(500) + .body("error!") + .unwrap() + .map(SdkBody::from); + let res = operation::Response::new(res); + let err = SdkError::service_error(UnmodeledError, res); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::TransientError)) + ); + } + + #[test] + fn classify_by_response_status_not_retryable() { + let policy = HttpStatusCodeClassifier::default(); + let res = http::Response::builder() + .status(408) + .body("error!") + .unwrap() + .map(SdkBody::from); + let res = operation::Response::new(res); + let err = SdkError::service_error(UnmodeledError, res); + + assert_eq!(policy.classify_error(&err), None); + } + + #[test] + fn classify_by_error_kind() { + struct ModeledRetries; + + impl ProvideErrorKind for ModeledRetries { + fn retryable_error_kind(&self) -> Option { + Some(ErrorKind::ClientError) + } + + fn code(&self) -> Option<&str> { + // code should not be called when `error_kind` is provided + unimplemented!() + } + } + + let policy = ModeledAsRetryableClassifier; + let res = http::Response::new("OK"); + let err = SdkError::service_error(ModeledRetries, res); + + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::ClientError)), + ); + } + + #[test] + fn classify_response_error() { + let policy = SmithyErrorClassifier; + let test_response = http::Response::new("OK").map(SdkBody::from); + let err: SdkError = + SdkError::response_error(UnmodeledError, operation::Response::new(test_response)); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::TransientError)), + ); + } + + #[test] + fn test_timeout_error() { + let policy = SmithyErrorClassifier; + let err: SdkError = SdkError::timeout_error("blah"); + assert_eq!( + policy.classify_error(&err), + Some(RetryReason::Error(ErrorKind::TransientError)), + ); + } +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs new file mode 100644 index 00000000000..6b7854fc2c2 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy.rs @@ -0,0 +1,8 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod never; + +pub use never::NeverRetryStrategy; diff --git a/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/never.rs b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/never.rs new file mode 100644 index 00000000000..49366a273dd --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/retries/strategy/never.rs @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_runtime_api::client::interceptors::InterceptorContext; +use aws_smithy_runtime_api::client::orchestrator::{BoxError, HttpRequest, HttpResponse}; +use aws_smithy_runtime_api::client::retries::{RetryStrategy, ShouldAttempt}; +use aws_smithy_runtime_api::config_bag::ConfigBag; + +#[derive(Debug, Clone, Default)] +pub struct NeverRetryStrategy {} + +impl NeverRetryStrategy { + pub fn new() -> Self { + Self::default() + } +} + +impl RetryStrategy for NeverRetryStrategy { + fn should_attempt_initial_request(&self, _cfg: &ConfigBag) -> Result { + Ok(ShouldAttempt::Yes) + } + + fn should_attempt_retry( + &self, + _context: &InterceptorContext, + _cfg: &ConfigBag, + ) -> Result { + Ok(ShouldAttempt::No) + } +} diff --git a/rust-runtime/aws-smithy-types/src/error/metadata.rs b/rust-runtime/aws-smithy-types/src/error/metadata.rs index 06925e13f9c..6629753733d 100644 --- a/rust-runtime/aws-smithy-types/src/error/metadata.rs +++ b/rust-runtime/aws-smithy-types/src/error/metadata.rs @@ -46,6 +46,12 @@ pub struct ErrorMetadata { extras: Option>, } +impl ProvideErrorMetadata for ErrorMetadata { + fn meta(&self) -> &ErrorMetadata { + self + } +} + /// Builder for [`ErrorMetadata`]. #[derive(Debug, Default)] pub struct Builder { diff --git a/tools/ci-build/Dockerfile b/tools/ci-build/Dockerfile index 2bce2b0ec9f..f5ed4869d92 100644 --- a/tools/ci-build/Dockerfile +++ b/tools/ci-build/Dockerfile @@ -139,7 +139,7 @@ ARG rust_nightly_version RUN cargo +${rust_nightly_version} -Z sparse-registry install cargo-wasi --locked --version ${cargo_wasi_version} FROM install_rust AS cargo_semver_checks -ARG cargo_semver_checks_version=0.19.0 +ARG cargo_semver_checks_version=0.20.0 ARG rust_nightly_version RUN cargo +${rust_nightly_version} -Z sparse-registry install cargo-semver-checks --locked --version ${cargo_semver_checks_version} # diff --git a/tools/ci-scripts/codegen-diff/semver-checks.py b/tools/ci-scripts/codegen-diff/semver-checks.py index cda783ce753..69ba9726d16 100755 --- a/tools/ci-scripts/codegen-diff/semver-checks.py +++ b/tools/ci-scripts/codegen-diff/semver-checks.py @@ -34,9 +34,16 @@ def main(skip_generation=False): os.chdir(sdk_directory) failed = False + # TODO(enableNewSmithyRuntime): Remove the deny list below + deny_list = [ + "aws-runtime", + "aws-runtime-api", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + ] for path in os.listdir(): eprint(f'checking {path}...', end='') - if get_cmd_status(f'git cat-file -e base:{sdk_directory}/{path}/Cargo.toml') == 0: + if path not in deny_list and get_cmd_status(f'git cat-file -e base:{sdk_directory}/{path}/Cargo.toml') == 0: (status, out, err) = get_cmd_output(f'cargo semver-checks check-release ' f'--baseline-rev {BASE_BRANCH} ' # in order to get semver-checks to work with publish-false crates, need to specify