google_cloud_metadata/
lib.rs

1use std::string;
2use std::time::Duration;
3
4use reqwest::header::{HeaderValue, USER_AGENT};
5use tokio::net::lookup_host;
6use tokio::sync::OnceCell;
7
8pub const METADATA_IP: &str = "169.254.169.254";
9pub const METADATA_HOST_ENV: &str = "GCE_METADATA_HOST";
10pub const METADATA_GOOGLE_HOST: &str = "metadata.google.internal:80";
11pub const METADATA_FLAVOR_KEY: &str = "Metadata-Flavor";
12pub const METADATA_GOOGLE: &str = "Google";
13
14static ON_GCE: OnceCell<bool> = OnceCell::const_new();
15
16static PROJECT_ID: OnceCell<String> = OnceCell::const_new();
17
18#[derive(thiserror::Error, Debug)]
19pub enum Error {
20    #[error("invalid response code: {0}")]
21    InvalidResponse(u16),
22    #[error(transparent)]
23    FromUTF8Error(#[from] string::FromUtf8Error),
24    #[error(transparent)]
25    HttpError(#[from] reqwest::Error),
26}
27
28pub async fn on_gce() -> bool {
29    match ON_GCE.get_or_try_init(test_on_gce).await {
30        Ok(s) => *s,
31        Err(_err) => false,
32    }
33}
34
35async fn test_on_gce() -> Result<bool, Error> {
36    // The user explicitly said they're on GCE, so trust them.
37    if std::env::var(METADATA_HOST_ENV).is_ok() {
38        return Ok(true);
39    }
40
41    let client = reqwest::Client::builder()
42        .timeout(Duration::from_secs(3))
43        .build()
44        .unwrap();
45    let url = format!("http://{METADATA_IP}");
46
47    let response = client.get(&url).send().await;
48    if let Ok(response) = response {
49        if response.status().is_success() {
50            let on_gce = match response.headers().get(METADATA_FLAVOR_KEY) {
51                None => false,
52                Some(s) => s == METADATA_GOOGLE,
53            };
54
55            if on_gce {
56                return Ok(true);
57            }
58        }
59    }
60
61    match lookup_host(METADATA_GOOGLE_HOST).await {
62        Ok(s) => {
63            for ip in s {
64                if ip.ip().to_string() == METADATA_IP {
65                    return Ok(true);
66                }
67            }
68        }
69        Err(_e) => return Ok(false),
70    };
71
72    Ok(false)
73}
74
75pub async fn project_id() -> String {
76    match PROJECT_ID
77        .get_or_try_init(|| get_etag_with_trim("project/project-id"))
78        .await
79    {
80        Ok(s) => s.to_string(),
81        Err(_err) => "".to_string(),
82    }
83}
84
85pub async fn email(service_account: &str) -> Result<String, Error> {
86    get_etag_with_trim(&format!("instance/service-accounts/{service_account}/email")).await
87}
88
89async fn get_etag_with_trim(suffix: &str) -> Result<String, Error> {
90    let result = get_etag(suffix).await?;
91    Ok(result.trim().to_string())
92}
93
94async fn get_etag(suffix: &str) -> Result<String, Error> {
95    let host = std::env::var(METADATA_HOST_ENV).unwrap_or_else(|_| METADATA_GOOGLE_HOST.to_string());
96    let url = format!("http://{host}/computeMetadata/v1/{suffix}");
97    let client = reqwest::Client::builder()
98        .timeout(Duration::from_secs(3))
99        .build()
100        .unwrap();
101    let response = client
102        .get(url)
103        .header(METADATA_FLAVOR_KEY, HeaderValue::from_str(METADATA_GOOGLE).unwrap())
104        .header(USER_AGENT, HeaderValue::from_str("gcloud-rust/0.1").unwrap())
105        .send()
106        .await?;
107
108    if response.status().is_success() {
109        return Ok(response.text().await?);
110    }
111    Err(Error::InvalidResponse(response.status().as_u16()))
112}