use std::{env, fmt, future::Future, str::FromStr, sync::Arc, time::Duration};
use http_body_util::{BodyExt as _, Empty};
use hyper::{
body::Incoming,
header::{HeaderName, HeaderValue, USER_AGENT},
http::{
response::Parts,
uri::{PathAndQuery, Scheme},
},
Request, StatusCode, Uri,
};
use hyper_util::client::legacy::connect::HttpConnector;
use tokio::sync::RwLock;
use tracing::trace;
macro_rules! __path {
($($expr:expr)*) => {
concat!("/computeMetadata/v1/", $($expr)*)
};
($expr:expr, $($tt:tt)*) => {
format!(__path!($expr), $($tt)*)
};
}
macro_rules! path {
($($expr:expr)*) => {
PathAndQuery::from_static(__path!($($expr)*))
};
($expr:expr, $($tt:tt)*) => {
PathAndQuery::from_str(&__path!($expr, $($tt)*))
};
}
macro_rules! impl_cache_fn {
($(#[$attr:meta])* $name:ident, $path:expr, $trim:expr) => {
$(#[$attr])*
pub async fn $name(&self) -> crate::Result<String> {
if let Some(value) = self.cache.$name.read().await.clone() {
return Ok(value);
}
let mut lock = self.cache.$name.write().await;
if let Some(value) = lock.clone() {
Ok(value)
} else {
let value = self.get(path!($path), $trim).await?;
*lock = Some(value.clone());
Ok(value)
}
}
};
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("http client error: {0}")]
Http(#[from] hyper_util::client::legacy::Error),
#[error("uri parse error: {0}")]
Uri(#[from] hyper::http::uri::InvalidUri),
#[error("response collection error: {0}")]
Response(#[from] hyper::Error),
#[error("response status code error: {0:?}")]
StatusCode((Parts, Incoming)),
#[error("response body encoding error: {0}")]
Encoding(#[from] std::string::FromUtf8Error),
#[error("response body deserialize error: {0}")]
Json(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone)]
struct Env {
metadata_host: Option<Uri>,
}
impl Env {
fn init() -> Self {
Self {
metadata_host: env::var("GCE_METADATA_HOST")
.ok()
.map(|s| Uri::from_str(&s).expect("`GCE_METADATA_HOST` is not valid URI")),
}
}
}
#[derive(Clone)]
struct Config {
schema: Scheme,
metadata_ip: Uri,
user_agent: HeaderValue,
flavor_name: HeaderName,
flavor_value: HeaderValue,
probe_timeout: Duration,
}
impl Default for Config {
fn default() -> Self {
let user_agent = concat!("rust-", env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
Self {
schema: Scheme::HTTP,
metadata_ip: Uri::from_static("169.254.169.254"),
user_agent: HeaderValue::from_static(user_agent),
flavor_name: HeaderName::from_static("metadata-flavor"),
flavor_value: HeaderValue::from_static("Google"),
probe_timeout: Duration::from_secs(5),
}
}
}
#[derive(Default)]
struct Cache {
on_gce: RwLock<Option<bool>>,
project_id: RwLock<Option<String>>,
numeric_project_id: RwLock<Option<String>>,
instance_id: RwLock<Option<String>>,
}
#[derive(Clone)]
pub struct Client {
inner: hyper_util::client::legacy::Client<HttpConnector, Empty<bytes::Bytes>>,
env: Env,
config: Config,
cache: Arc<Cache>,
}
impl Client {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let inner = {
let keepalive = Duration::from_secs(30);
let mut connector = HttpConnector::new();
connector.set_connect_timeout(Some(Duration::from_secs(2)));
connector.set_keepalive(Some(keepalive));
hyper_util::client::legacy::Builder::new(hyper_util::rt::TokioExecutor::new())
.pool_idle_timeout(keepalive)
.build(connector)
};
Client { inner, env: Env::init(), config: Default::default(), cache: Default::default() }
}
fn get_parts(
&self,
path_and_query: PathAndQuery,
) -> impl Future<Output = crate::Result<(Parts, Incoming)>> + Send + 'static {
let host = self.env.metadata_host.clone();
let mut parts = host.unwrap_or_else(|| self.config.metadata_ip.clone()).into_parts();
parts.scheme = Some(self.config.schema.clone());
parts.path_and_query = Some(path_and_query);
let uri = Uri::from_parts(parts).unwrap();
let req = Request::get(uri)
.header(USER_AGENT, &self.config.user_agent)
.header(&self.config.flavor_name, &self.config.flavor_value)
.body(Empty::<bytes::Bytes>::new())
.unwrap();
let fut = self.inner.request(req);
async {
let parts = fut.await?.into_parts();
match parts.0.status {
StatusCode::OK => Ok(parts),
_ => Err(Error::StatusCode(parts)),
}
}
}
pub fn get(
&self,
path_and_query: PathAndQuery,
trim: bool,
) -> impl Future<Output = crate::Result<String>> + Send + 'static {
let fut = self.get_parts(path_and_query);
async move {
let (_, body) = fut.await?;
let mut s = String::from_utf8(body.collect().await?.to_bytes().into())?;
if trim {
let trimed = s.trim();
if trimed.len() != s.len() {
s = trimed.to_owned();
}
}
Ok(s)
}
}
pub fn get_as<T>(&self, path_and_query: PathAndQuery) -> impl Future<Output = crate::Result<T>> + Send + 'static
where
T: serde::de::DeserializeOwned,
{
use bytes::Buf as _;
let fut = self.get_parts(path_and_query);
async {
let (_, body) = fut.await?;
Ok(serde_json::from_reader(body.collect().await?.to_bytes().reader())?)
}
}
pub async fn on_gce(&self) -> crate::Result<bool> {
if let Some(on) = *self.cache.on_gce.read().await {
return Ok(on);
}
let mut on_gce = self.cache.on_gce.write().await;
if let Some(on) = *on_gce {
return Ok(on);
}
let present = self.env.metadata_host.is_some();
trace!("check environment variable: {}", present);
if present {
*on_gce = Some(true);
return Ok(true);
}
let meta = async {
let mut parts = self.config.metadata_ip.clone().into_parts();
parts.scheme = Some(self.config.schema.clone());
parts.path_and_query = Some(PathAndQuery::from_static("/"));
let req = Request::get(Uri::from_parts(parts).unwrap())
.header(USER_AGENT, &self.config.user_agent)
.header(&self.config.flavor_name, &self.config.flavor_value)
.body(Empty::<bytes::Bytes>::new())
.unwrap();
let on = self
.inner
.request(req)
.await
.map(|resp| resp.headers().get(&self.config.flavor_name) == Some(&self.config.flavor_value))
.unwrap_or(false);
trace!("access to medatada service: {}", on);
on
};
let name = tokio::task::spawn_blocking(|| {
use std::net::ToSocketAddrs as _;
let on = ("metadata.google.internal", 0).to_socket_addrs().map(|addrs| addrs.len() > 0).unwrap_or(false);
trace!("resolve hostname: {}", on);
on
});
let on = tokio::select! {
true = meta => true,
Ok(true) = name => true,
_ = tokio::time::sleep(self.config.probe_timeout) => {
trace!("probe timeout exceeded");
false
},
};
*on_gce = Some(on);
Ok(on)
}
impl_cache_fn!(
project_id,
"project/project-id",
true
);
impl_cache_fn!(
numeric_project_id,
"project/numeric-project-id",
true
);
pub async fn internal_ip(&self) -> crate::Result<String> {
self.get(path!("instance/network-interfaces/0/ip"), true).await
}
pub async fn external_ip(&self) -> crate::Result<String> {
self.get(path!("instance/network-interfaces/0/access-configs/0/external-ip"), true).await
}
pub async fn email(&self, sa: Option<&str>) -> crate::Result<String> {
let path = match sa {
Some(sa) => path!("instance/service-accounts/{}/email", sa)?,
_ => path!("instance/service-accounts/default/email"),
};
self.get(path, true).await
}
pub async fn hostname(&self) -> crate::Result<String> {
self.get(path!("instance/hostname"), true).await
}
pub async fn instance_tags(&self) -> crate::Result<Vec<String>> {
self.get_as(path!("instance/tags")).await
}
impl_cache_fn!(
instance_id,
"instance/id",
true
);
pub async fn instance_name(&self) -> crate::Result<String> {
self.get(path!("instance/name"), true).await
}
pub async fn zone(&self) -> crate::Result<String> {
let s = self.get(path!("instance/zone"), true).await?;
Ok(s.split('/').last().unwrap_or("").to_owned())
}
pub async fn instance_attrs(&self) -> crate::Result<Vec<String>> {
let s = self.get(path!("instance/attributes/"), false).await?;
Ok(s.lines().map(ToOwned::to_owned).collect())
}
pub async fn project_attrs(&self) -> crate::Result<Vec<String>> {
let s = self.get(path!("project/attributes/"), false).await?;
Ok(s.lines().map(ToOwned::to_owned).collect())
}
pub async fn instance_attr(&self, attr: impl AsRef<str>) -> crate::Result<String> {
self.get(path!("instance/attributes/{}", attr.as_ref())?, false).await
}
pub async fn project_attr(&self, attr: impl AsRef<str>) -> crate::Result<String> {
self.get(path!("project/attributes/{}", attr.as_ref())?, false).await
}
pub async fn scopes(&self, sa: Option<&str>) -> crate::Result<Vec<String>> {
let path = match sa {
Some(sa) => path!("instance/service-accounts/{}/scopes", sa)?,
_ => path!("instance/service-accounts/default/scopes"),
};
let s = self.get(path, true).await?;
Ok(s.lines().map(ToOwned::to_owned).collect())
}
}
impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client").finish()
}
}