Skip to content

Commit

Permalink
Add DefaultEndpointResolver to the orchestrator (#2577)
Browse files Browse the repository at this point in the history
## Motivation and Context
This PR adds `DefaultEndpointResolver`, a default implementer of the
[EndpointResolver](https://github.com/awslabs/smithy-rs/blob/1e27efe05fe7b991c9f9bbf3d63a297b2dded334/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs#L182-L184)
trait, to the orchestrator.

## Description
Roughly speaking, the endpoint-related work is currently done by
`make_operation` and `SmithyEndpointStage`, where `make_operation`
constructs endpoint params and applies an endpoint resolver and the
`SmithyEndpointStage` later updates the request header based on the
resolved endpoint. In the orchestrator world, that work is handled by
`DefaultEndpointResolver::resolve_and_apply_endpoint`.

The way endpoint parameters and an endpoint prefix are made available to
`DefaultEndpointResolver::resolve_and_apply_endpoint` is done by
interceptors because they _may_ require pieces of information only
available in an operation input struct. The place that has access to
both an operation input struct and a config bag happens to be an
interceptor.

There are two interceptors `<Operation>EndpointParamsInterceptor` and
`<Operation>EndpointParamsFinalizerInterceptor` per operation. We pass
around endpoint params _builder_ through interceptors to allow it to be
configured with more information at a later point; An end point
parameters builder is first initialized within the
`ServiceRuntimePlugin` with the field values obtained from the service
config, and is stored in a config bag. The finalizer interceptor "seals"
the builder and creates the actual endpoint params before it is passed
to `DefaultEndpointResolver::resolve_and_apply_endpoint`. These
interceptors implement `read_before_execution` because they need to
access an operation input before it gets serialized (otherwise,
`context.input()` would return a `None`).

## Testing
Replaced `StaticUriEndpointResolver` with `DefaultEndpointResolver` in
`sra_test` and verified the test continued passing.

UPDATE: The test currently fails in the `main` branch (it returns a
`None` when extracting `SigV4OperationSigningConfig` from the config bag
in `OverrideSigningTimeInterceptor`, hence `unwrap` fails), and by
merging the `main` branch this PR no longer passes the test, but it does
not add new failures either.

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._

---------

Co-authored-by: Yuki Saito <[email protected]>
  • Loading branch information
2 people authored and unexge committed Apr 24, 2023
1 parent 1a2dcd3 commit 0e9b4a7
Show file tree
Hide file tree
Showing 25 changed files with 559 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package software.amazon.smithy.rustsdk
import software.amazon.smithy.aws.traits.auth.SigV4Trait
import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait
import software.amazon.smithy.model.knowledge.ServiceIndex
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.traits.OptionalAuthTrait
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
Expand Down Expand Up @@ -38,6 +39,7 @@ class SigV4AuthDecorator : ClientCodegenDecorator {

override fun operationRuntimePluginCustomizations(
codegenContext: ClientCodegenContext,
operation: OperationShape,
baseCustomizations: List<OperationRuntimePluginCustomization>,
): List<OperationRuntimePluginCustomization> =
baseCustomizations.letIf(codegenContext.settings.codegenConfig.enableNewSmithyRuntime) {
Expand Down
24 changes: 12 additions & 12 deletions aws/sra-test/integration-tests/aws-sdk-s3/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
aws-credential-types = { path = "../../../rust-runtime/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../../rust-runtime/aws-http" }
aws-runtime = { path = "../../../rust-runtime/aws-runtime" }
aws-sdk-s3 = { path = "../../build/sdk/aws-sdk-s3", features = ["test-util"] }
aws-sigv4 = { path = "../../../rust-runtime/aws-sigv4" }
aws-types = { path = "../../../rust-runtime/aws-types" }
aws-smithy-async = { path = "../../../../rust-runtime/aws-smithy-async", features = ["rt-tokio"] }
aws-smithy-client = { path = "../../../../rust-runtime/aws-smithy-client", features = ["test-util"] }
aws-smithy-types = { path = "../../../../rust-runtime/aws-smithy-types" }
aws-smithy-http = { path = "../../../../rust-runtime/aws-smithy-http" }
aws-smithy-runtime = { path = "../../../../rust-runtime/aws-smithy-runtime", features = ["test-util"] }
aws-smithy-runtime-api = { path = "../../../../rust-runtime/aws-smithy-runtime-api" }
aws-credential-types = { path = "../../../sdk/build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-http = { path = "../../../sdk/build/aws-sdk/sdk/aws-http" }
aws-runtime = { path = "../../../sdk/build/aws-sdk/sdk/aws-runtime" }
aws-sdk-s3 = { path = "../../../sdk/build/aws-sdk/sdk/s3/", features = ["test-util"] }
aws-sigv4 = { path = "../../../sdk/build/aws-sdk/sdk/aws-sigv4" }
aws-types = { path = "../../../sdk/build/aws-sdk/sdk/aws-types" }
aws-smithy-async = { path = "../../../sdk/build/aws-sdk/sdk/aws-smithy-async", features = ["rt-tokio"] }
aws-smithy-client = { path = "../../../sdk/build/aws-sdk/sdk/aws-smithy-client", features = ["test-util"] }
aws-smithy-types = { path = "../../../sdk/build/aws-sdk/sdk/aws-smithy-types" }
aws-smithy-http = { path = "../../../sdk/build/aws-sdk/sdk/aws-smithy-http" }
aws-smithy-runtime = { path = "../../../sdk/build/aws-sdk/sdk/aws-smithy-runtime", features = ["test-util"] }
aws-smithy-runtime-api = { path = "../../../sdk/build/aws-sdk/sdk/aws-smithy-runtime-api" }
tokio = { version = "1.23.1", features = ["macros", "test-util", "rt-multi-thread"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.15", features = ["env-filter", "json"] }
Expand Down
94 changes: 87 additions & 7 deletions aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ use aws_sdk_s3::primitives::SdkBody;
use aws_smithy_client::erase::DynConnector;
use aws_smithy_client::test_connection::TestConnection;
use aws_smithy_runtime::client::connections::adapter::DynConnectorAdapter;
use aws_smithy_runtime_api::client::endpoints::StaticUriEndpointResolver;
use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext, Interceptors};
use aws_smithy_runtime_api::client::endpoints::DefaultEndpointResolver;
use aws_smithy_runtime_api::client::interceptors::{
Interceptor, InterceptorContext, InterceptorError, Interceptors,
};
use aws_smithy_runtime_api::client::orchestrator::{
BoxError, ConfigBagAccessors, Connection, HttpRequest, HttpResponse, TraceProbe,
};
Expand All @@ -27,7 +29,6 @@ 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};

Expand Down Expand Up @@ -108,9 +109,16 @@ async fn sra_manual_test() {
),
);

cfg.set_endpoint_resolver(StaticUriEndpointResolver::uri(Uri::from_static(
"https://test-bucket.s3.us-east-1.amazonaws.com/",
)));
cfg.set_endpoint_resolver(DefaultEndpointResolver::new(
aws_smithy_http::endpoint::SharedEndpointResolver::new(
aws_sdk_s3::endpoint::DefaultResolver::new(),
),
));

let params_builder = aws_sdk_s3::endpoint::Params::builder()
.set_region(Some("us-east-1".to_owned()))
.set_endpoint(Some("https://s3.us-east-1.amazonaws.com/".to_owned()));
cfg.put(params_builder);

cfg.set_retry_strategy(
aws_smithy_runtime_api::client::retries::NeverRetryStrategy::new(),
Expand Down Expand Up @@ -162,6 +170,77 @@ async fn sra_manual_test() {
}
}

// This is a temporary operation runtime plugin until <Operation>EndpointParamsInterceptor and
// <Operation>EndpointParamsFinalizerInterceptor have been fully implemented, in which case
// `.with_operation_plugin(ManualOperationRuntimePlugin)` can be removed.
struct ManualOperationRuntimePlugin;

impl RuntimePlugin for ManualOperationRuntimePlugin {
fn configure(&self, cfg: &mut ConfigBag) -> Result<(), BoxError> {
#[derive(Debug)]
struct ListObjectsV2EndpointParamsInterceptor;
impl Interceptor<HttpRequest, HttpResponse> for ListObjectsV2EndpointParamsInterceptor {
fn read_before_execution(
&self,
context: &InterceptorContext<HttpRequest, HttpResponse>,
cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
let input = context.input()?;
let input = input
.downcast_ref::<ListObjectsV2Input>()
.ok_or_else(|| InterceptorError::invalid_input_access())?;
let mut params_builder = cfg
.get::<aws_sdk_s3::endpoint::ParamsBuilder>()
.ok_or(InterceptorError::read_before_execution(
"missing endpoint params builder",
))?
.clone();
params_builder = params_builder.set_bucket(input.bucket.clone());
cfg.put(params_builder);

Ok(())
}
}

#[derive(Debug)]
struct ListObjectsV2EndpointParamsFinalizerInterceptor;
impl Interceptor<HttpRequest, HttpResponse> for ListObjectsV2EndpointParamsFinalizerInterceptor {
fn read_before_execution(
&self,
_context: &InterceptorContext<HttpRequest, HttpResponse>,
cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
let params_builder = cfg
.get::<aws_sdk_s3::endpoint::ParamsBuilder>()
.ok_or(InterceptorError::read_before_execution(
"missing endpoint params builder",
))?
.clone();
let params = params_builder
.build()
.map_err(InterceptorError::read_before_execution)?;
cfg.put(
aws_smithy_runtime_api::client::orchestrator::EndpointResolverParams::new(
params,
),
);

Ok(())
}
}

cfg.get::<Interceptors<HttpRequest, HttpResponse>>()
.expect("interceptors set")
.register_operation_interceptor(
Arc::new(ListObjectsV2EndpointParamsInterceptor) as _
)
.register_operation_interceptor(Arc::new(
ListObjectsV2EndpointParamsFinalizerInterceptor,
) as _);
Ok(())
}
}

let conn = TestConnection::new(vec![(
http::Request::builder()
.header("authorization", "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=ae78f74d26b6b0c3a403d9e8cc7ec3829d6264a2b33db672bf2b151bbb901786")
Expand All @@ -187,7 +266,8 @@ async fn sra_manual_test() {

let runtime_plugins = aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugins::new()
.with_client_plugin(ManualServiceRuntimePlugin(conn.clone()))
.with_operation_plugin(aws_sdk_s3::operation::list_objects_v2::ListObjectsV2::new());
.with_operation_plugin(aws_sdk_s3::operation::list_objects_v2::ListObjectsV2::new())
.with_operation_plugin(ManualOperationRuntimePlugin);

let input = ListObjectsV2Input::builder()
.bucket("test-bucket")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegen
import software.amazon.smithy.rust.codegen.client.smithy.customize.CombinedClientCodegenDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customize.NoOpEventStreamSigningDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customize.RequiredCustomizations
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointParamsDecorator
import software.amazon.smithy.rust.codegen.client.smithy.endpoint.EndpointsDecorator
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDecorator
import software.amazon.smithy.rust.codegen.client.testutil.ClientDecoratableBuildPlugin
Expand Down Expand Up @@ -58,6 +59,7 @@ class RustClientCodegenPlugin : ClientDecoratableBuildPlugin() {
RequiredCustomizations(),
FluentClientDecorator(),
EndpointsDecorator(),
EndpointParamsDecorator(),
NoOpEventStreamSigningDecorator(),
ApiKeyAuthDecorator(),
*decorator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ package software.amazon.smithy.rust.codegen.client.smithy.customizations

import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.traits.EndpointTrait
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.client.smithy.generators.EndpointTraitBindings
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.withBlock
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.customize.OperationCustomization
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSection

class EndpointPrefixGenerator(private val codegenContext: CodegenContext, private val shape: OperationShape) :
class EndpointPrefixGenerator(private val codegenContext: ClientCodegenContext, private val shape: OperationShape) :
OperationCustomization() {
override fun section(section: OperationSection): Writable = when (section) {
is OperationSection.MutateRequest -> writable {
Expand All @@ -29,11 +29,16 @@ class EndpointPrefixGenerator(private val codegenContext: CodegenContext, privat
epTrait,
)
withBlock("let endpoint_prefix = ", "?;") {
endpointTraitBindings.render(this, "self")
endpointTraitBindings.render(
this,
"self",
codegenContext.settings.codegenConfig.enableNewSmithyRuntime,
)
}
rust("request.properties_mut().insert(endpoint_prefix);")
}
}

else -> emptySection
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ interface ClientCodegenDecorator : CoreCodegenDecorator<ClientCodegenContext> {
*/
fun operationRuntimePluginCustomizations(
codegenContext: ClientCodegenContext,
operation: OperationShape,
baseCustomizations: List<OperationRuntimePluginCustomization>,
): List<OperationRuntimePluginCustomization> = baseCustomizations
}
Expand Down Expand Up @@ -135,10 +136,11 @@ open class CombinedClientCodegenDecorator(decorators: List<ClientCodegenDecorato

override fun operationRuntimePluginCustomizations(
codegenContext: ClientCodegenContext,
operation: OperationShape,
baseCustomizations: List<OperationRuntimePluginCustomization>,
): List<OperationRuntimePluginCustomization> =
combineCustomizations(baseCustomizations) { decorator, customizations ->
decorator.operationRuntimePluginCustomizations(codegenContext, customizations)
decorator.operationRuntimePluginCustomizations(codegenContext, operation, customizations)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ internal class EndpointConfigCustomization(
/// use aws_smithy_http::endpoint;
/// use $moduleUseName::endpoint::{Params as EndpointParams, DefaultResolver};
/// /// Endpoint resolver which adds a prefix to the generated endpoint
/// ##[derive(Debug)]
/// struct PrefixResolver {
/// base_resolver: DefaultResolver,
/// prefix: String
Expand Down Expand Up @@ -132,6 +133,7 @@ internal class EndpointConfigCustomization(
RuntimeType.forInlineFun("MissingResolver", ClientRustModule.Endpoint) {
rustTemplate(
"""
##[derive(Debug)]
pub(crate) struct MissingResolver;
impl<T> #{ResolveEndpoint}<T> for MissingResolver {
fn resolve_endpoint(&self, _params: &T) -> #{Result} {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.client.smithy.endpoint

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.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

/**
* Decorator that injects operation-level interceptors that configure an endpoint parameters builder
* with operation specific information, e.g. a bucket name.
*
* Whenever a setter needs to be called on the endpoint parameters builder with operation specific information,
* this decorator must be used.
*/
class EndpointParamsDecorator : ClientCodegenDecorator {
override val name: String get() = "EndpointParamsDecorator"
override val order: Byte get() = 0

override fun operationRuntimePluginCustomizations(
codegenContext: ClientCodegenContext,
operation: OperationShape,
baseCustomizations: List<OperationRuntimePluginCustomization>,
): List<OperationRuntimePluginCustomization> =
baseCustomizations.letIf(codegenContext.settings.codegenConfig.enableNewSmithyRuntime) {
it + listOf(EndpointParametersRuntimePluginCustomization(codegenContext, operation))
}
}

private class EndpointParametersRuntimePluginCustomization(
private val codegenContext: ClientCodegenContext,
private val operation: OperationShape,
) : OperationRuntimePluginCustomization() {
override fun section(section: OperationRuntimePluginSection): Writable = writable {
val symbolProvider = codegenContext.symbolProvider
val operationName = symbolProvider.toSymbol(operation).name
if (section is OperationRuntimePluginSection.AdditionalConfig) {
section.registerInterceptor(codegenContext.runtimeConfig, this) {
rust("${operationName}EndpointParamsInterceptor")
}
// The finalizer interceptor should be registered last
section.registerInterceptor(codegenContext.runtimeConfig, this) {
rust("${operationName}EndpointParamsFinalizerInterceptor")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class EndpointTypesGenerator(
}

fun paramsStruct(): RuntimeType = EndpointParamsGenerator(params).paramsStruct()
fun paramsBuilder(): RuntimeType = EndpointParamsGenerator(params).paramsBuilder()
fun defaultResolver(): RuntimeType? =
rules?.let { EndpointResolverGenerator(stdlib, runtimeConfig).defaultEndpointResolver(it) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ internal class EndpointParamsGenerator(private val parameters: Parameters) {
generateEndpointsStruct(this)
}

private fun endpointsBuilder(): RuntimeType = RuntimeType.forInlineFun("ParamsBuilder", ClientRustModule.Endpoint) {
internal fun paramsBuilder(): RuntimeType = RuntimeType.forInlineFun("ParamsBuilder", ClientRustModule.Endpoint) {
generateEndpointParamsBuilder(this)
}

Expand Down Expand Up @@ -182,7 +182,7 @@ internal class EndpointParamsGenerator(private val parameters: Parameters) {
#{Builder}::default()
}
""",
"Builder" to endpointsBuilder(),
"Builder" to paramsBuilder(),
)
parameters.toList().forEach { parameter ->
val name = parameter.memberName()
Expand Down
Loading

0 comments on commit 0e9b4a7

Please sign in to comment.