use ureq::http::Response;
use ureq::{Agent as HttpClient, Body, Error as HttpClientError};
use thiserror::Error;
use super::utils::{blocking_client, non_empty};
const IMDS_BASE: &str = "http://169.254.169.254";
const IMDS_TOKEN_PATH: &str = "latest/api/token";
const IMDS_IDENTITY_DOCUMENT_PATH: &str = "dynamic/instance-identity/document";
const IMDS_TTL_HEADER: &str = "X-aws-ec2-metadata-token-ttl-seconds";
const IMDS_TOKEN_HEADER: &str = "X-aws-ec2-metadata-token";
const IMDS_TOKEN_TTL: &str = "60";
const IMDS_TIMEOUT_SECS: u64 = 1;
#[derive(Debug, Error)]
pub(super) enum ImdsError {
#[error("Could not retrieve an IMDSv2 auth token: {0}")]
AuthToken(#[source] HttpClientError),
#[error("The IMDSv2 auth token endpoint answered with an empty body")]
EmptyAuthToken,
#[error("Could not GET {url}: {error}")]
GetRequest {
url: String,
#[source]
error: HttpClientError,
},
#[error("Could not read text response: {0}")]
TextResponseRead(#[source] HttpClientError),
#[error("Could not read JSON response: {0}")]
JsonResponseRead(#[source] HttpClientError),
}
pub(super) trait ImdsProvider {
fn get(&self, path: &str) -> Result<String, ImdsError>;
fn get_identity_document(&self) -> Result<InstanceIdentityDocument, ImdsError>;
}
pub(super) struct ImdsClient {
client: HttpClient,
token: String,
}
impl ImdsClient {
pub(super) fn new() -> Result<Self, ImdsError> {
let client = blocking_client(std::time::Duration::from_secs(IMDS_TIMEOUT_SECS));
let token = client
.put(format!("{IMDS_BASE}/{IMDS_TOKEN_PATH}"))
.header(IMDS_TTL_HEADER, IMDS_TOKEN_TTL)
.send_empty()
.and_then(|mut r| r.body_mut().read_to_string())
.map_err(ImdsError::AuthToken)?;
let token = non_empty(token).ok_or(ImdsError::EmptyAuthToken)?;
Ok(Self { client, token })
}
fn get_response(&self, path: &str) -> Result<Response<Body>, ImdsError> {
let url = format!("{IMDS_BASE}/latest/{path}");
self.client
.get(&url)
.header(IMDS_TOKEN_HEADER, &self.token)
.call()
.map_err(|error| ImdsError::GetRequest { url, error })
}
fn get_json<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T, ImdsError> {
self.get_response(path)?
.body_mut()
.read_json()
.map_err(ImdsError::JsonResponseRead)
}
}
impl ImdsProvider for ImdsClient {
fn get(&self, path: &str) -> Result<String, ImdsError> {
self.get_response(&format!("meta-data/{path}"))?
.body_mut()
.read_to_string()
.map_err(ImdsError::TextResponseRead)
}
fn get_identity_document(&self) -> Result<InstanceIdentityDocument, ImdsError> {
self.get_json(IMDS_IDENTITY_DOCUMENT_PATH)
}
}
#[derive(Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct InstanceIdentityDocument {
#[cfg(any(feature = "detector-aws-ec2", feature = "detector-aws-eks"))]
pub account_id: Option<String>,
#[cfg(any(feature = "detector-aws-ec2", feature = "detector-aws-eks"))]
pub region: Option<String>,
pub availability_zone: Option<String>,
pub instance_id: Option<String>,
pub instance_type: Option<String>,
pub image_id: Option<String>,
architecture: Option<String>,
}
impl InstanceIdentityDocument {
pub(super) fn host_arch(&self) -> Option<&'static str> {
match self.architecture.as_deref() {
Some("x86_64") => Some("amd64"),
Some("arm64") => Some("arm64"),
Some("i386") => Some("x86"),
_ => None,
}
}
}
#[cfg(test)]
pub(super) mod tests {
use std::collections::HashMap;
use super::*;
pub struct FakeImdsClient {
document: &'static str,
gets: HashMap<&'static str, &'static str>,
}
impl FakeImdsClient {
pub fn new() -> Self {
Self {
document: "",
gets: HashMap::new(),
}
}
pub fn with_document(mut self, json: &'static str) -> Self {
self.document = json;
self
}
pub fn with_get(mut self, path: &'static str, value: &'static str) -> Self {
self.gets.insert(path, value);
self
}
}
impl ImdsProvider for FakeImdsClient {
fn get(&self, path: &str) -> Result<String, ImdsError> {
self.gets
.get(&path)
.map(|&s| s.to_owned())
.ok_or(ImdsError::GetRequest {
url: path.to_owned(),
error: HttpClientError::StatusCode(404),
})
}
fn get_identity_document(&self) -> Result<InstanceIdentityDocument, ImdsError> {
serde_json::from_str(self.document)
.map_err(HttpClientError::Json)
.map_err(ImdsError::JsonResponseRead)
}
}
const DOC_X86_64: &str = r#"{ "architecture": "x86_64" }"#;
const DOC_ARM64: &str = r#"{ "architecture": "arm64" }"#;
const DOC_I386: &str = r#"{ "architecture": "i386" }"#;
const DOC_MIPS: &str = r#"{ "architecture": "mips" }"#;
const DOC_EMPTY_ARCH: &str = r#"{ "architecture": "" }"#;
const DOC_NO_ARCH: &str = r#"{}"#;
#[test]
fn host_arch_known_mappings() {
let doc: InstanceIdentityDocument = serde_json::from_str(DOC_X86_64).unwrap();
assert_eq!(doc.host_arch(), Some("amd64"));
let doc: InstanceIdentityDocument = serde_json::from_str(DOC_ARM64).unwrap();
assert_eq!(doc.host_arch(), Some("arm64"));
let doc: InstanceIdentityDocument = serde_json::from_str(DOC_I386).unwrap();
assert_eq!(doc.host_arch(), Some("x86"));
}
#[test]
fn host_arch_unknown_or_absent() {
let doc: InstanceIdentityDocument = serde_json::from_str(DOC_NO_ARCH).unwrap();
assert_eq!(doc.host_arch(), None);
let doc: InstanceIdentityDocument = serde_json::from_str(DOC_MIPS).unwrap();
assert_eq!(doc.host_arch(), None);
let doc: InstanceIdentityDocument = serde_json::from_str(DOC_EMPTY_ARCH).unwrap();
assert_eq!(doc.host_arch(), None);
}
}