Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user agent #79

Merged
merged 20 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions aws-s3-transfer-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ aws-config = { version = "1.5.6", features = ["behavior-version-latest"] }
aws-sdk-s3 = { version = "1.51.0", features = ["behavior-version-latest"] }
aws-smithy-async = "1.2.1"
aws-smithy-experimental = { version = "0.1.3", features = ["crypto-aws-lc"] }
aws-smithy-runtime-api = "1.7.1"
aws-smithy-runtime-api = "1.7.3"
aws-runtime = "1.4.4"
aws-smithy-types = "1.2.6"
aws-types = "1.3.3"
blocking = "1.6.0"
Expand All @@ -32,7 +33,7 @@ walkdir = "2"
[dev-dependencies]
aws-sdk-s3 = { version = "1.51.0", features = ["behavior-version-latest", "test-util"] }
aws-smithy-mocks-experimental = "0.2.1"
aws-smithy-runtime = { version = "1.7.1", features = ["client", "connector-hyper-0-14-x", "test-util", "wire-mock"] }
aws-smithy-runtime = { version = "1.7.4", features = ["client", "connector-hyper-0-14-x", "test-util", "wire-mock"] }
clap = { version = "4.5.7", default-features = false, features = ["derive", "std", "help"] }
console-subscriber = "0.4.0"
http-02x = { package = "http", version = "0.2.9" }
Expand Down
4 changes: 4 additions & 0 deletions aws-s3-transfer-manager/examples/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use aws_s3_transfer_manager::metrics::Throughput;
use aws_s3_transfer_manager::operation::download::body::Body;
use aws_s3_transfer_manager::types::{ConcurrencySetting, PartSize};
use aws_sdk_s3::error::DisplayErrorContext;
use aws_types::app_name::AppName;
use bytes::Buf;
use clap::{CommandFactory, Parser};
use tokio::fs;
Expand Down Expand Up @@ -151,6 +152,7 @@ async fn do_download(args: Args) -> Result<(), BoxError> {
let tm_config = aws_s3_transfer_manager::from_env()
.concurrency(ConcurrencySetting::Explicit(args.concurrency))
.part_size(PartSize::Target(args.part_size))
.app_name(Some(AppName::new("transfer_manager_example_cp").unwrap()))
.load()
.await;

Expand Down Expand Up @@ -228,6 +230,7 @@ async fn do_upload(args: Args) -> Result<(), BoxError> {
let tm_config = aws_s3_transfer_manager::from_env()
.concurrency(ConcurrencySetting::Explicit(args.concurrency))
.part_size(PartSize::Target(args.part_size))
.app_name(Some(AppName::new("transfer_manager_example_cp").unwrap()))
.load()
.await;

Expand Down Expand Up @@ -278,6 +281,7 @@ async fn main() -> Result<(), BoxError> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_thread_ids(true)
.with_ansi(false) // Disable ANSI colors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the ansi color, it's unreadable from pure text. I think it will be easier to read and parse.

.init();
}

Expand Down
3 changes: 2 additions & 1 deletion aws-s3-transfer-manager/external-types.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ allowed_external_types = [
"bytes::bytes::Bytes",
"bytes::buf::buf_impl::Buf",
"aws_types::request_id::RequestId",
"aws_types::request_id::RequestIdExt"
"aws_types::request_id::RequestIdExt",
"aws_types::app_name::AppName",
TingDaoK marked this conversation as resolved.
Show resolved Hide resolved
]
22 changes: 22 additions & 0 deletions aws-s3-transfer-manager/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

use aws_types::app_name::AppName;

use crate::metrics::unit::ByteUnit;
use crate::types::{ConcurrencySetting, PartSize};
use std::cmp;
Expand All @@ -18,6 +20,7 @@ pub struct Config {
multipart_threshold: PartSize,
target_part_size: PartSize,
concurrency: ConcurrencySetting,
app_name: Option<AppName>,
client: aws_sdk_s3::client::Client,
}

Expand All @@ -43,6 +46,11 @@ impl Config {
&self.concurrency
}

/// Returns the name of the app that is using the transfer manager, if it was provided.
pub fn app_name(&self) -> Option<&AppName> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix: I don't think we want to expose app name. (1) we allow taking an S3 client (today anyway) that can already have app name configured, (2) I don't think this is what mountpoint will be looking for out of a UA header.

Is there something I'm missing here about why we are exposing this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my reading of the sdk, the app name is just the string for the SDK to add in the user agent with some format check.

  1. I think if we take the s3 client and want to respect the setting from transfer manager, we will be adding the user agent setting from transfer manager to the passed in client as the app name.

  2. what do you think mountpoint will be looking for?
    I assume they just want a string, and I thought the app name is the wrap of string for SDK to be provided in the user agent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my reading of the sdk, the app name is just the string for the SDK to add in the user agent with some format check.

App name (appId) comes from the UA SEP. It's meant for end user applications (though I've yet to see someone use it 🤷‍♂️ in any SDK). Mountpoint could go through that but they are more like framework metadata.

I think if we take the s3 client and want to respect the setting from transfer manager, we will be adding the user agent setting from transfer manager to the passed in client as the app name.

The issue is the user could have set app name on the client. I think maybe we just leave this off for now and gather requirements from mountpoint to guide us. I think this is their user agent implementation though. From the looks of it we capture a lot of the md/* fields they are gathering I think so maybe they just need lib/mountpoint-s3-client#<version>.

Copy link
Contributor Author

@TingDaoK TingDaoK Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the framework metadata is supposed to be used as frameworks owned by the SDKs.

For frameworks owned by the SDKs, framework metadata MUST be included in the header following this format: "lib" / name ["#" version]. Additional metadata MAY be appended.

It seems to be an implementation detail for SDKs to me. And mountpoint is more like the application user, but yeah, they are more like a framework for the end user?
I guess we can chat with them and see what are they expecting?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we run into this in other places. I think the SEP is worded poorly here. e.g. Amplify isn't owned by us but they need the same ability with the Kotlin SDK to tack on their UA info (which they utilize framework metadata IIRC).

self.app_name.as_ref()
}

/// The Amazon S3 client instance that will be used to send requests to S3.
pub fn client(&self) -> &aws_sdk_s3::Client {
&self.client
Expand All @@ -55,6 +63,7 @@ pub struct Builder {
multipart_threshold_part_size: PartSize,
target_part_size: PartSize,
concurrency: ConcurrencySetting,
app_name: Option<AppName>,
client: Option<aws_sdk_s3::Client>,
}

Expand Down Expand Up @@ -122,8 +131,20 @@ impl Builder {
self
}

/// Sets the name of the app that is using the client.
///
/// This _optional_ name is used to identify the application in the user agent that
/// gets sent along with requests.
pub fn app_name(mut self, app_name: Option<AppName>) -> Self {
self.app_name = app_name;
self
}

/// Set an explicit S3 client to use.
pub fn client(mut self, client: aws_sdk_s3::Client) -> Self {
// TODO - decide the approach here:
// - Convert the client to build to modify it based on other configs for transfer manager
// - Instead of taking the client, take sdk-config/s3-config/builder?
self.client = Some(client);
self
}
Expand All @@ -134,6 +155,7 @@ impl Builder {
multipart_threshold: self.multipart_threshold_part_size,
target_part_size: self.target_part_size,
concurrency: self.concurrency,
app_name: self.app_name,
client: self.client.expect("client set"),
}
}
Expand Down
77 changes: 72 additions & 5 deletions aws-s3-transfer-manager/src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,37 @@
* SPDX-License-Identifier: Apache-2.0
*/

use aws_config::BehaviorVersion;
use aws_runtime::sdk_feature::AwsSdkFeature;
use aws_sdk_s3::config::{Intercept, IntoShared};
use aws_types::app_name::AppName;

use crate::config::Builder;
use crate::{
http,
types::{ConcurrencySetting, PartSize},
Config,
};

#[derive(Debug)]
struct TransferManagerFeatureInterceptor;

impl Intercept for TransferManagerFeatureInterceptor {
fn name(&self) -> &'static str {
"TransferManagerFeature"
TingDaoK marked this conversation as resolved.
Show resolved Hide resolved
}

fn read_before_execution(
&self,
_ctx: &aws_sdk_s3::config::interceptors::BeforeSerializationInterceptorContextRef<'_>,
cfg: &mut aws_sdk_s3::config::ConfigBag,
) -> Result<(), aws_sdk_s3::error::BoxError> {
cfg.interceptor_state()
.store_append::<AwsSdkFeature>(AwsSdkFeature::S3Transfer);
TingDaoK marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
}
}

/// Load transfer manager [`Config`] from the environment.
#[derive(Default, Debug)]
pub struct ConfigLoader {
Expand Down Expand Up @@ -52,17 +76,60 @@ impl ConfigLoader {
self
}

/// Sets the name of the app that is using the client.
///
/// This _optional_ name is used to identify the application in the user agent that
/// gets sent along with requests.
pub fn app_name(mut self, app_name: Option<AppName>) -> Self {
self.builder = self.builder.app_name(app_name);
self
}

/// Load the default configuration
///
/// If fields have been overridden during builder construction, the override values will be
/// used. Otherwise, the default values for each field will be provided.
pub async fn load(self) -> Config {
let shared_config = aws_config::from_env()
.http_client(http::default_client())
let mut config_loader =
aws_config::defaults(BehaviorVersion::latest()).http_client(http::default_client());
if let Some(app_name) = self.builder.app_name.as_ref() {
config_loader = config_loader.app_name(app_name.clone());
}
let shared_config = config_loader.load().await;

let mut sdk_client_builder = aws_sdk_s3::config::Builder::from(&shared_config);
sdk_client_builder.push_interceptor(TransferManagerFeatureInterceptor.into_shared());
let builder = self
.builder
.client(aws_sdk_s3::Client::from_conf(sdk_client_builder.build()));
builder.build()
}
}

#[cfg(test)]
mod tests {
use crate::types::{ConcurrencySetting, PartSize};
use aws_sdk_s3::config::Intercept;
use aws_types::app_name::AppName;

#[tokio::test]
async fn load_with_app_name_and_interceptor() {
TingDaoK marked this conversation as resolved.
Show resolved Hide resolved
let expected_app_name = "bananas";

let config = crate::from_env()
.concurrency(ConcurrencySetting::Explicit(123))
.part_size(PartSize::Target(8))
.app_name(Some(AppName::new(expected_app_name).unwrap()))
.load()
.await;
let s3_client = aws_sdk_s3::Client::new(&shared_config);
let builder = self.builder.client(s3_client);
builder.build()
let sdk_s3_config = config.client().config();
assert_eq!(
sdk_s3_config.app_name().unwrap().as_ref(),
expected_app_name
);
let tm_interceptor_exists = sdk_s3_config
.interceptors()
.any(|item| item.name() == "TransferManagerFeature");
assert!(tm_interceptor_exists);
}
}
Loading