-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add instance and config metadata to user agent
We'd like to collect the same metadata that AWS SDKs gather so we can better understand how different Mountpoint features are used. This change adds support for detecting platform and instance metadata and including it in HTTP User-agents. We follow the SDK template for serializing this metadata. To make this cleaner, I moved the instance info logic into the client crate so that all users can get this kind of user agent. The new `UserAgent` struct supports addings arbitrary key/value pairs, and we use that in Mountpoint to record basic configurations. User agents are always a bit annoying to test, but I manually verified in a few cases (caching enabled/disabled) that this change was sending the expected headers. Signed-off-by: James Bornholt <[email protected]>
- Loading branch information
1 parent
a1e4d86
commit 8b32850
Showing
11 changed files
with
353 additions
and
131 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
//! A simple interface to retrieve information about the EC2 instance this client is running on by | ||
//! querying the Instance Metadata Service (IMDS). | ||
use std::env; | ||
|
||
use once_cell::unsync::Lazy; | ||
use thiserror::Error; | ||
|
||
use crate::imds_crt_client::{IdentityDocument, ImdsCrtClient, ImdsQueryRequestError}; | ||
|
||
/// Information on the EC2 instance from the IMDS client. The client is queried lazily and only if | ||
/// the `AWS_EC2_METADATA_DISABLED` environment variable is not set. | ||
#[derive(Debug)] | ||
pub struct InstanceInfo { | ||
document: Lazy<Result<IdentityDocument, InstanceInfoError>>, | ||
} | ||
|
||
impl InstanceInfo { | ||
/// Create a new instance. The IMDS client will only be queried when a methon on the instance is | ||
/// called, and only if the `AWS_EC2_METADATA_DISABLED` environment variable is not set. | ||
pub fn new() -> Self { | ||
Self { | ||
document: Lazy::new(|| { | ||
if !imds_disabled() { | ||
match retrieve_instance_identity_document() { | ||
Ok(identity_document) => { | ||
tracing::debug!(?identity_document, "got instance info from IMDS"); | ||
Ok(identity_document) | ||
} | ||
Err(err) => { | ||
tracing::warn!("EC2 instance info not retrieved: {err:?}"); | ||
Err(err) | ||
} | ||
} | ||
} else { | ||
tracing::debug!("EC2 instance info not retrieved: IMDS was disabled"); | ||
Err(InstanceInfoError::ImdsDisabled) | ||
} | ||
}), | ||
} | ||
} | ||
|
||
/// The region for the current instance, if it can be retrieved using the IMDS client. | ||
pub fn region(&self) -> Result<&str, &InstanceInfoError> { | ||
self.document.as_ref().map(|d| d.region.as_str()) | ||
} | ||
|
||
/// The instance type for the current instance, if it can be retrieved using the IMDS client. | ||
pub fn instance_type(&self) -> Result<&str, &InstanceInfoError> { | ||
self.document.as_ref().map(|d| d.instance_type.as_str()) | ||
} | ||
} | ||
|
||
impl Default for InstanceInfo { | ||
fn default() -> Self { | ||
Self::new() | ||
} | ||
} | ||
|
||
fn retrieve_instance_identity_document() -> Result<IdentityDocument, InstanceInfoError> { | ||
let imds_crt_client = ImdsCrtClient::new().map_err(InstanceInfoError::ImdsClientFailed)?; | ||
|
||
let identity_document = futures::executor::block_on(imds_crt_client.get_identity_document())?; | ||
Ok(identity_document) | ||
} | ||
|
||
fn imds_disabled() -> bool { | ||
match env::var_os("AWS_EC2_METADATA_DISABLED") { | ||
Some(val) => val.to_ascii_lowercase() != "false", | ||
None => false, | ||
} | ||
} | ||
|
||
/// Errors returned by instance info queries | ||
#[derive(Debug, Error)] | ||
pub enum InstanceInfoError { | ||
/// IMDS is disabled | ||
#[error("IMDS is disabled")] | ||
ImdsDisabled, | ||
|
||
/// A query to IMDS failed | ||
#[error("IMDS query failed: {0}")] | ||
ImdsQueryFailed(#[from] ImdsQueryRequestError), | ||
|
||
/// The IMDS client couldn't be constructed | ||
#[error("could not construct IMDS client: {0}")] | ||
ImdsClientFailed(mountpoint_s3_crt::common::error::Error), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
//! Utilities to construct a HTTP User-agent header in the AWS SDK format | ||
use platform_info::{PlatformInfo, PlatformInfoAPI, UNameAPI}; | ||
|
||
use crate::build_info; | ||
use crate::instance_info::InstanceInfo; | ||
|
||
/// A builder for AWS SDK-style user agent headers | ||
#[derive(Debug, Clone)] | ||
pub struct UserAgent { | ||
fields: Vec<String>, | ||
prefix: Option<String>, | ||
} | ||
|
||
impl UserAgent { | ||
/// Create a new User-agent builder | ||
pub fn new(prefix: Option<String>) -> Self { | ||
Self { fields: vec![], prefix } | ||
} | ||
|
||
/// Create a new User-agent builder with the default platform metadata fields | ||
pub fn new_with_instance_info(prefix: Option<String>, instance_info: &InstanceInfo) -> Self { | ||
let user_agent_info = UserAgentInfo::new(instance_info); | ||
Self::new_with_user_agent_info(prefix, user_agent_info) | ||
} | ||
|
||
fn new_with_user_agent_info(prefix: Option<String>, user_agent_info: UserAgentInfo) -> Self { | ||
let mut fields = vec![]; | ||
|
||
if let Some(sysname) = user_agent_info.sysname { | ||
if let Some(release) = user_agent_info.release { | ||
fields.push(format!( | ||
"os/{}#{}", | ||
sanitize_string(canonicalize_sysname(sysname)), | ||
sanitize_string(release) | ||
)); | ||
} else { | ||
fields.push(format!("os/{}", sanitize_string(sysname))); | ||
} | ||
} | ||
|
||
if let Some(machine) = user_agent_info.machine { | ||
fields.push(format!("md/arch#{}", sanitize_string(machine))); | ||
} | ||
|
||
if let Some(instance_type) = user_agent_info.instance_type { | ||
fields.push(format!("md/instance#{}", sanitize_string(instance_type))); | ||
} | ||
|
||
Self { fields, prefix } | ||
} | ||
|
||
/// Add a key-value metadata field to the header | ||
pub fn key_value(&mut self, key: &str, value: &str) -> &mut Self { | ||
self.fields | ||
.push(format!("md/{}#{}", sanitize_string(key), sanitize_string(value))); | ||
self | ||
} | ||
|
||
/// Add a value-only metadata field to the header | ||
pub fn value(&mut self, value: &str) -> &mut Self { | ||
self.fields.push(format!("md/{}", sanitize_string(value))); | ||
self | ||
} | ||
|
||
/// Construct the final User-agent header string | ||
pub fn build(self) -> String { | ||
let mut fields = Vec::with_capacity(self.fields.len() + 2); | ||
if let Some(prefix) = self.prefix { | ||
fields.push(prefix); | ||
} | ||
fields.push(format!("mountpoint-s3-client/{}", build_info::FULL_VERSION)); | ||
fields.extend(self.fields); | ||
fields.join(" ") | ||
} | ||
} | ||
|
||
fn sanitize_string(s: impl AsRef<str>) -> String { | ||
const VALID_CHARS: &[char] = &['!', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~']; | ||
s.as_ref() | ||
.replace(|c: char| !c.is_alphanumeric() && !VALID_CHARS.contains(&c), "-") | ||
} | ||
|
||
fn canonicalize_sysname(sysname: impl AsRef<str>) -> &'static str { | ||
match sysname.as_ref() { | ||
"Linux" => "linux", | ||
"Darwin" => "macos", | ||
// https://github.com/uutils/platform-info/blob/755cdc7d597469962a08a3f88f838c7cc8d2c0cb/src/platform/windows.rs#L523 | ||
"Windows_NT" => "Windows", | ||
_ => "other", | ||
} | ||
} | ||
|
||
/// To make this code testable we factor out the platform queries so we can mock them in tests | ||
struct UserAgentInfo { | ||
sysname: Option<String>, | ||
release: Option<String>, | ||
machine: Option<String>, | ||
instance_type: Option<String>, | ||
} | ||
|
||
impl UserAgentInfo { | ||
fn new(instance_info: &InstanceInfo) -> Self { | ||
let platform_info = PlatformInfo::new().ok(); | ||
|
||
Self { | ||
sysname: platform_info | ||
.as_ref() | ||
.map(|p| p.sysname().to_string_lossy().into_owned()), | ||
release: platform_info | ||
.as_ref() | ||
.map(|p| p.release().to_string_lossy().into_owned()), | ||
machine: platform_info | ||
.as_ref() | ||
.map(|p| p.machine().to_string_lossy().into_owned()), | ||
instance_type: instance_info.instance_type().ok().map(|s| s.to_string()), | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_platform_fields() { | ||
// Linux ip-172-31-29-144.us-west-2.compute.internal 6.1.61-85.141.amzn2023.aarch64 #1 SMP Wed Nov 8 00:38:50 UTC 2023 aarch64 aarch64 aarch64 GNU/Linux | ||
let user_agent_info = UserAgentInfo { | ||
sysname: Some("Linux".to_string()), | ||
release: Some("6.1.61-85.141.amzn2023.aarch64".to_string()), | ||
machine: Some("aarch64".to_string()), | ||
instance_type: None, | ||
}; | ||
let user_agent = UserAgent::new_with_user_agent_info(None, user_agent_info).build(); | ||
assert!(user_agent.contains("os/linux#6.1.61-85.141.amzn2023.aarch64 md/arch#aarch64")); | ||
assert!(user_agent.starts_with("mountpoint-s3-client/")); | ||
|
||
let user_agent_info = UserAgentInfo { | ||
sysname: Some("Linux".to_string()), | ||
release: Some("6.1.61-85.141.amzn2023.aarch64".to_string()), | ||
machine: Some("aarch64".to_string()), | ||
instance_type: Some("t4g.large".to_string()), | ||
}; | ||
let user_agent = UserAgent::new_with_user_agent_info(Some("prefix".to_string()), user_agent_info).build(); | ||
assert!(user_agent.contains("os/linux#6.1.61-85.141.amzn2023.aarch64 md/arch#aarch64 md/instance#t4g.large")); | ||
assert!(user_agent.starts_with("prefix mountpoint-s3-client/")); | ||
|
||
// Darwin abcdefg.amazon.com 23.1.0 Darwin Kernel Version 23.1.0: Mon Oct 9 21:27:24 PDT 2023; root:xnu-10002.41.9~6/RELEASE_ARM64_T6000 arm64 | ||
let user_agent_info = UserAgentInfo { | ||
sysname: Some("Darwin".to_string()), | ||
release: Some("23.1.0".to_string()), | ||
machine: Some("arm64".to_string()), | ||
instance_type: None, | ||
}; | ||
let user_agent = UserAgent::new_with_user_agent_info(None, user_agent_info).build(); | ||
assert!(user_agent.contains("os/macos#23.1.0 md/arch#arm64")); | ||
assert!(user_agent.starts_with("mountpoint-s3-client/")); | ||
} | ||
|
||
#[test] | ||
fn test_sanitize() { | ||
assert_eq!( | ||
sanitize_string("Java_HotSpot_(TM)_64-Bit_Server_VM"), | ||
"Java_HotSpot_-TM-_64-Bit_Server_VM" | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.