use crate::errors::{Error, Result};
use chrono::{DateTime, Utc};
use reqwest::ClientBuilder;
use serde::{Deserialize, Serialize};
use tokio::time::Duration;
pub async fn fetch_instance_id() -> Result<String> {
fetch_metadata_by_path("instance-id").await
}
pub async fn fetch_public_hostname() -> Result<String> {
fetch_metadata_by_path("public-hostname").await
}
pub async fn fetch_public_ipv4() -> Result<String> {
fetch_metadata_by_path("public-ipv4").await
}
pub async fn fetch_availability_zone() -> Result<String> {
fetch_metadata_by_path("placement/availability-zone").await
}
pub async fn fetch_spot_instance_action() -> Result<InstanceAction> {
let resp = fetch_metadata_by_path("spot/instance-action").await?;
serde_json::from_slice(resp.as_bytes()).map_err(|e| Error::Other {
message: format!(
"failed to parse spot/instance-action response '{}' {:?}",
resp, e
),
retryable: false,
})
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct InstanceAction {
pub action: String,
#[serde(with = "rfc_manager::serde_format::rfc_3339")]
pub time: DateTime<Utc>,
}
pub async fn fetch_region() -> Result<String> {
let mut az = fetch_availability_zone().await?;
az.truncate(az.len() - 1);
Ok(az)
}
pub async fn fetch_metadata_by_path(path: &str) -> Result<String> {
log::info!("fetching meta-data/{}", path);
let token = fetch_token().await?;
let uri = format!("http://169.254.169.254/latest/meta-data/{}", path);
let cli = ClientBuilder::new()
.user_agent(env!("CARGO_PKG_NAME"))
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(15))
.connection_verbose(true)
.build()
.map_err(|e| Error::API {
message: format!("failed ClientBuilder build {:?}", e),
retryable: false,
})?;
let resp = cli
.get(&uri)
.header("X-aws-ec2-metadata-token", token)
.send()
.await
.map_err(|e| Error::API {
message: format!("failed to build GET meta-data/{} {:?}", path, e),
retryable: false,
})?;
let out = resp.bytes().await.map_err(|e| Error::API {
message: format!("failed to read bytes {:?}", e),
retryable: false,
})?;
let out: Vec<u8> = out.into();
match String::from_utf8(out) {
Ok(text) => Ok(text),
Err(e) => Err(Error::API {
message: format!("GET meta-data/{} failed String::from_utf8 ({})", path, e),
retryable: false,
}),
}
}
const IMDS_V2_SESSION_TOKEN_URI: &str = "http://169.254.169.254/latest/api/token";
async fn fetch_token() -> Result<String> {
log::info!("fetching IMDS v2 token");
let cli = ClientBuilder::new()
.user_agent(env!("CARGO_PKG_NAME"))
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(15))
.connection_verbose(true)
.build()
.map_err(|e| Error::API {
message: format!("failed ClientBuilder build {:?}", e),
retryable: false,
})?;
let resp = cli
.put(IMDS_V2_SESSION_TOKEN_URI)
.header("X-aws-ec2-metadata-token-ttl-seconds", "21600")
.send()
.await
.map_err(|e| Error::API {
message: format!("failed to build PUT api/token {:?}", e),
retryable: false,
})?;
let out = resp.bytes().await.map_err(|e| Error::API {
message: format!("failed to read bytes {:?}", e),
retryable: false,
})?;
let out: Vec<u8> = out.into();
match String::from_utf8(out) {
Ok(text) => Ok(text),
Err(e) => Err(Error::API {
message: format!("GET token failed String::from_utf8 ({})", e),
retryable: false,
}),
}
}