google_cloud_metadata/
lib.rs1use 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 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}