Unverified Commit a85cef6d authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Add configurable runtime plugin to send_v2 so that it is usable (#2635)



## Motivation and Context
- `send_v2()` doesn't work for a very small number of reasons—allow it
one last final runtime_plugin so that we can use it to make an e2e
request

## Description
- update a few decorators to be deterministic
## Testing
<!--- Please describe in detail how you tested your changes -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

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

----

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

---------

Co-authored-by: default avatarJohn DiSanti <jdisanti@amazon.com>
Co-authored-by: default avatarZelda Hessler <zhessler@amazon.com>
parent e3ecb6ce
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -196,6 +196,7 @@ pub mod sigv4 {
            &self,
            request: &mut HttpRequest,
            identity: &Identity,
            // TODO(enableNewSmithyRuntime): should this be the config bag?
            signing_properties: &PropertyBag,
        ) -> Result<(), BoxError> {
            let operation_config = signing_properties
+29 −10
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@ const AMZ_SDK_INVOCATION_ID: HeaderName = HeaderName::from_static("amz-sdk-invoc
#[non_exhaustive]
#[derive(Debug)]
pub struct InvocationIdInterceptor {
    id: HeaderValue,
    id: InvocationId,
}

impl InvocationIdInterceptor {
@@ -29,12 +29,9 @@ impl InvocationIdInterceptor {

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 }
        Self {
            id: InvocationId::from_uuid(),
        }
    }
}

@@ -45,11 +42,33 @@ impl Interceptor<HttpRequest, HttpResponse> for InvocationIdInterceptor {
        _cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        let headers = context.request_mut()?.headers_mut();
        headers.append(AMZ_SDK_INVOCATION_ID, self.id.clone());
        let id = _cfg.get::<InvocationId>().unwrap_or(&self.id);
        headers.append(AMZ_SDK_INVOCATION_ID, id.0.clone());
        Ok(())
    }
}

/// InvocationId provides a consistent ID across retries
#[derive(Debug)]
pub struct InvocationId(HeaderValue);
impl InvocationId {
    /// A test invocation id to allow deterministic requests
    pub fn for_tests() -> Self {
        InvocationId(HeaderValue::from_static(
            "00000000-0000-4000-8000-000000000000",
        ))
    }

    fn from_uuid() -> Self {
        let id = Uuid::new_v4();
        let id = id
            .to_string()
            .parse()
            .expect("UUIDs always produce a valid header value");
        Self(id)
    }
}

#[cfg(test)]
mod tests {
    use crate::invocation_id::InvocationIdInterceptor;
@@ -84,8 +103,8 @@ mod tests {
            .unwrap();

        let header = expect_header(&context, "amz-sdk-invocation-id");
        assert_eq!(&interceptor.id, header);
        assert_eq!(&interceptor.id.0, header);
        // UUID should include 32 chars and 4 dashes
        assert_eq!(interceptor.id.len(), 36);
        assert_eq!(interceptor.id.0.len(), 36);
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ 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-client = { path = "../../../../rust-runtime/aws-smithy-client", features = ["test-util", "rustls"] }
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"] }
+105 −0
Original line number Diff line number Diff line
{
  "events": [
    {
      "connection_id": 0,
      "action": {
        "Request": {
          "request": {
            "uri": "https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~",
            "headers": {
              "x-amz-security-token": [
                "notarealsessiontoken"
              ],
              "authorization": [
                "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent, Signature=e7eccf4e792113f5f17a50bfd8f1719479e89ba0b476894e6f3dba030dc87f82"
              ],
              "x-amz-user-agent": [
                "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0"
              ],
              "x-amz-date": [
                "20210618T170728Z"
              ],
              "x-amz-content-sha256": [
                "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
              ],
              "amz-sdk-invocation-id": [
                "00000000-0000-4000-8000-000000000000"
              ],
              "user-agent": [
                "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
              ]
            },
            "method": "GET"
          }
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Response": {
          "response": {
            "Ok": {
              "status": 200,
              "version": "HTTP/1.1",
              "headers": {
                "x-amz-request-id": [
                  "9X5E7C9EAB6AQEP2"
                ],
                "x-amz-id-2": [
                  "gZsrBxajPyo1Q0DE2plGf7T6kAnxd4Xx7/S+8lq18GegL6kFbnVXLLh1LnBzpEpFiHN9XoNHkeA="
                ],
                "content-type": [
                  "application/xml"
                ],
                "transfer-encoding": [
                  "chunked"
                ],
                "server": [
                  "AmazonS3"
                ],
                "date": [
                  "Wed, 26 Apr 2023 14:00:24 GMT"
                ],
                "x-amz-bucket-region": [
                  "us-east-1"
                ]
              }
            }
          }
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Data": {
          "data": {
            "Utf8": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ListBucketResult>\n    <Name>test-bucket</Name>\n    <Prefix>prefix~</Prefix>\n    <KeyCount>1</KeyCount>\n    <MaxKeys>1000</MaxKeys>\n    <IsTruncated>false</IsTruncated>\n    <Contents>\n        <Key>some-file.file</Key>\n        <LastModified>2009-10-12T17:50:30.000Z</LastModified>\n        <Size>434234</Size>\n        <StorageClass>STANDARD</StorageClass>\n    </Contents>\n</ListBucketResult>"
          },
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Response"
        }
      }
    }
  ],
  "docs": "Test sending an S3 ListObjectsV2 operation with a successful response.",
  "version": "V0"
}
+45 −252
Original line number Diff line number Diff line
@@ -3,282 +3,75 @@
 * SPDX-License-Identifier: Apache-2.0
 */

use aws_credential_types::cache::{CredentialsCache, SharedCredentialsCache};
use aws_credential_types::provider::SharedCredentialsProvider;
use aws_http::user_agent::{ApiMetadata, AwsUserAgent};
use aws_runtime::recursion_detection::RecursionDetectionInterceptor;
use aws_runtime::user_agent::UserAgentInterceptor;
use aws_http::user_agent::AwsUserAgent;
use aws_runtime::invocation_id::InvocationId;
use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::endpoint::Params;
use aws_sdk_s3::operation::list_objects_v2::{
    ListObjectsV2Error, ListObjectsV2Input, ListObjectsV2Output,
};
use aws_sdk_s3::primitives::SdkBody;

use aws_sdk_s3::Client;

use aws_smithy_client::dvr;
use aws_smithy_client::dvr::MediaType;
use aws_smithy_client::erase::DynConnector;
use aws_smithy_client::test_connection::TestConnection;
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, Interceptors};
use aws_smithy_runtime_api::client::orchestrator::{
    BoxError, ConfigBagAccessors, Connection, HttpRequest, HttpResponse, RequestTime, TraceProbe,
};
use aws_smithy_runtime_api::client::retries::RetryClassifiers;

use aws_smithy_runtime_api::client::orchestrator::{ConfigBagAccessors, RequestTime};
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};

// TODO(orchestrator-test): unignore
#[ignore]
use std::time::{Duration, SystemTime, UNIX_EPOCH};

const LIST_BUCKETS_PATH: &str = "test-data/list-objects-v2.json";

#[tokio::test]
async fn sra_test() {
    tracing_subscriber::fmt::init();

    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")
            .uri("https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~")
            .body(SdkBody::empty())
            .unwrap(),
        http::Response::builder().status(200).body("").unwrap(),
    )]);
    let conn = dvr::ReplayingConnection::from_file(LIST_BUCKETS_PATH).unwrap();

    let config = aws_sdk_s3::Config::builder()
        .credentials_provider(Credentials::for_tests())
        .region(Region::new("us-east-1"))
        .http_connector(conn.clone())
        .http_connector(DynConnector::new(conn.clone()))
        .build();
    let client = aws_sdk_s3::Client::from_conf(config);
    let client = Client::from_conf(config);
    let fixup = FixupPlugin {
        client: client.clone(),
        timestamp: UNIX_EPOCH + Duration::from_secs(1624036048),
    };

    let _ = dbg!(
    let resp = dbg!(
        client
            .list_objects_v2()
            .config_override(aws_sdk_s3::Config::builder().force_path_style(false))
            .bucket("test-bucket")
            .prefix("prefix~")
            .send_v2()
            .send_v2_with_plugin(Some(fixup))
            .await
    );

    conn.assert_requests_match(&[]);
}

// TODO(orchestrator-test): replace with the above once runtime plugin config works
#[tokio::test]
async fn sra_manual_test() {
    tracing_subscriber::fmt::init();

    struct ManualServiceRuntimePlugin {
        connector: TestConnection<&'static str>,
        endpoint_resolver: SharedEndpointResolver<Params>,
        credentials_cache: SharedCredentialsCache,
    // To regenerate the test:
    // conn.dump_to_file("test-data/list-objects-v2.json").unwrap();
    let resp = resp.expect("valid e2e test");
    assert_eq!(resp.name(), Some("test-bucket"));
    conn.full_validate(MediaType::Xml).await.expect("failed")
}

    impl RuntimePlugin for ManualServiceRuntimePlugin {
        fn configure(&self, cfg: &mut ConfigBag) -> Result<(), BoxError> {
            let http_auth_schemes =
                aws_smithy_runtime_api::client::orchestrator::HttpAuthSchemes::builder()
                    .auth_scheme(
                        aws_runtime::auth::sigv4::SCHEME_ID,
                        aws_runtime::auth::sigv4::SigV4HttpAuthScheme::new(),
                    )
                    .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(),
                ),
            );

            cfg.set_endpoint_resolver(DefaultEndpointResolver::new(self.endpoint_resolver.clone()));

            let params_builder = 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::client::retries::strategy::NeverRetryStrategy::default(),
            );

            let connection: Box<dyn Connection> = Box::new(DynConnectorAdapter::new(
                DynConnector::new(self.connector.clone()),
            ));
            cfg.set_connection(connection);

            cfg.set_trace_probe({
                #[derive(Debug)]
                struct StubTraceProbe;
                impl TraceProbe for StubTraceProbe {
                    fn dispatch_events(&self) {
                        // no-op
                    }
                }
                StubTraceProbe
            });

            cfg.put(SigningService::from_static("s3"));
            cfg.put(SigningRegion::from(Region::from_static("us-east-1")));
            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::<Interceptors<HttpRequest, HttpResponse>>()
                .expect("interceptors set")
                .register_client_interceptor(Arc::new(UserAgentInterceptor::new()) 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(())
struct FixupPlugin {
    client: Client,
    timestamp: SystemTime,
}
    }

    // 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(
impl RuntimePlugin for FixupPlugin {
    fn configure(
        &self,
                    context: &InterceptorContext<HttpRequest, HttpResponse>,
        cfg: &mut ConfigBag,
                ) -> Result<(), BoxError> {
                    let input = context.input()?;
                    let input = input
                        .downcast_ref::<ListObjectsV2Input>()
                        .ok_or_else(|| "failed to downcast to ListObjectsV2Input")?;
                    let mut params_builder = cfg
                        .get::<aws_sdk_s3::endpoint::ParamsBuilder>()
                        .ok_or("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("missing endpoint params builder")?
                        .clone();
                    let params = params_builder.build().map_err(|err| {
                        ContextAttachedError::new("endpoint params could not be built", err)
                    })?;
                    cfg.put(
                        aws_smithy_runtime_api::client::orchestrator::EndpointResolverParams::new(
                            params,
                        ),
                    );

                    Ok(())
                }
            }
    ) -> Result<(), aws_smithy_runtime_api::client::runtime_plugin::BoxError> {
        let params_builder = Params::builder()
            .set_region(self.client.conf().region().map(|c| c.as_ref().to_string()))
            .bucket("test-bucket");

            cfg.get::<Interceptors<HttpRequest, HttpResponse>>()
                .expect("interceptors set")
                .register_operation_interceptor(
                    Arc::new(ListObjectsV2EndpointParamsInterceptor) as _
                )
                .register_operation_interceptor(Arc::new(
                    ListObjectsV2EndpointParamsFinalizerInterceptor,
                ) as _);
        cfg.put(params_builder);
        cfg.set_request_time(RequestTime::new(self.timestamp.clone()));
        cfg.put(AwsUserAgent::for_tests());
        cfg.put(InvocationId::for_tests());
        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")
                    .uri("https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=prefix~")
                    .body(SdkBody::empty())
                    .unwrap(),
                http::Response::builder().status(200).body(r#"<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult>
    <Name>test-bucket</Name>
    <Prefix>prefix~</Prefix>
    <KeyCount>1</KeyCount>
    <MaxKeys>1000</MaxKeys>
    <IsTruncated>false</IsTruncated>
    <Contents>
        <Key>some-file.file</Key>
        <LastModified>2009-10-12T17:50:30.000Z</LastModified>
        <Size>434234</Size>
        <StorageClass>STANDARD</StorageClass>
    </Contents>
</ListBucketResult>
"#).unwrap(),
            )]);

    let endpoint_resolver =
        SharedEndpointResolver::new(aws_sdk_s3::endpoint::DefaultResolver::new());
    let credentials_cache = SharedCredentialsCache::new(
        CredentialsCache::lazy()
            .create_cache(SharedCredentialsProvider::new(Credentials::for_tests())),
    );

    let service_runtime_plugin = ManualServiceRuntimePlugin {
        connector: conn.clone(),
        endpoint_resolver,
        credentials_cache,
    };

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

    let input = ListObjectsV2Input::builder()
        .bucket("test-bucket")
        .prefix("prefix~")
        .build()
        .unwrap();
    let input = TypedBox::new(input).erase();
    let output = aws_smithy_runtime::client::orchestrator::invoke(input, &runtime_plugins)
        .await
        .map_err(|err| {
            err.map_service_error(|err| {
                TypedBox::<ListObjectsV2Error>::assume_from(err)
                    .expect("correct error type")
                    .unwrap()
            })
        })
        .unwrap();
    let output = TypedBox::<ListObjectsV2Output>::assume_from(output)
        .expect("correct output type")
        .unwrap();
    dbg!(output);

    conn.assert_requests_match(&[]);
}
Loading