pub mod slo;
pub mod trace;
use std::env;
use std::path::PathBuf;
use anyhow::{Context, Result};
use reqwest::Client;
use serde::Deserialize;
pub struct GcpCredentials {
pub project_id: String,
pub credentials_path: PathBuf,
}
impl GcpCredentials {
pub fn is_available() -> bool {
find_credentials().is_some() && get_project_id().is_some()
}
pub fn from_env() -> Option<Self> {
let credentials_path = find_credentials()?;
let project_id = get_project_id()?;
Some(Self {
project_id,
credentials_path,
})
}
pub async fn access_token(&self, client: &Client) -> Result<String> {
let contents = std::fs::read_to_string(&self.credentials_path)
.context("Failed to read GCP credentials file")?;
let creds: AdcFile =
serde_json::from_str(&contents).context("Failed to parse GCP credentials file")?;
match creds.r#type.as_deref() {
Some("authorized_user") => refresh_user_token(client, &creds).await,
Some("service_account") => anyhow::bail!(
"Service account key signing is not supported. \
Run `gcloud auth application-default login` to use user credentials."
),
other => anyhow::bail!("Unsupported GCP credential type: {:?}", other),
}
}
}
pub fn find_credentials() -> Option<PathBuf> {
if let Ok(path) = env::var("GOOGLE_APPLICATION_CREDENTIALS") {
let p = PathBuf::from(path);
if p.exists() {
return Some(p);
}
}
if let Ok(home) = env::var("HOME") {
let adc = PathBuf::from(home).join(".config/gcloud/application_default_credentials.json");
if adc.exists() {
return Some(adc);
}
}
if let Ok(appdata) = env::var("APPDATA") {
let adc = PathBuf::from(appdata).join("gcloud/application_default_credentials.json");
if adc.exists() {
return Some(adc);
}
}
None
}
pub fn get_project_id() -> Option<String> {
for var in &["GOOGLE_CLOUD_PROJECT", "GCP_PROJECT", "GCLOUD_PROJECT"] {
if let Ok(p) = env::var(var) {
if !p.is_empty() {
return Some(p);
}
}
}
if let Some(creds_path) = find_credentials() {
if let Ok(contents) = std::fs::read_to_string(&creds_path) {
if let Ok(creds) = serde_json::from_str::<AdcFile>(&contents) {
if let Some(p) = creds.quota_project_id {
return Some(p);
}
}
}
}
get_project_from_gcloud_config()
}
fn get_project_from_gcloud_config() -> Option<String> {
let home = env::var("HOME").ok()?;
let gcloud_dir = PathBuf::from(&home).join(".config/gcloud");
let active_config = std::fs::read_to_string(gcloud_dir.join("active_config"))
.ok()
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "default".to_string());
let config_path = gcloud_dir
.join("configurations")
.join(format!("config_{}", active_config));
let contents = std::fs::read_to_string(config_path).ok()?;
for line in contents.lines() {
let line = line.trim();
if line.starts_with("project") {
if let Some(value) = line.split('=').nth(1) {
let p = value.trim().to_string();
if !p.is_empty() {
return Some(p);
}
}
}
}
None
}
async fn refresh_user_token(client: &Client, creds: &AdcFile) -> Result<String> {
let refresh_token = creds
.refresh_token
.as_ref()
.context("No refresh_token in GCP credentials file")?;
let client_id = creds
.client_id
.as_ref()
.context("No client_id in GCP credentials file")?;
let client_secret = creds
.client_secret
.as_ref()
.context("No client_secret in GCP credentials file")?;
let resp = client
.post("https://oauth2.googleapis.com/token")
.form(&[
("grant_type", "refresh_token"),
("refresh_token", refresh_token.as_str()),
("client_id", client_id.as_str()),
("client_secret", client_secret.as_str()),
])
.send()
.await
.context("Failed to call Google OAuth2 token endpoint")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
if body.contains("invalid_grant") || body.contains("invalid_rapt") {
anyhow::bail!(
"GCP credentials appear expired. \
Run `gcloud auth application-default login` to refresh them."
);
}
anyhow::bail!("OAuth2 token refresh failed: {} — {}", status, body);
}
let token_resp: TokenResponse = resp
.json()
.await
.context("Failed to parse OAuth2 token response")?;
Ok(token_resp.access_token)
}
#[derive(Debug, Deserialize)]
pub(crate) struct AdcFile {
#[serde(rename = "type")]
pub r#type: Option<String>,
pub quota_project_id: Option<String>,
pub refresh_token: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
}
#[derive(Debug, Deserialize)]
struct TokenResponse {
access_token: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn availability_does_not_panic() {
let _ = GcpCredentials::is_available();
}
#[test]
fn from_env_returns_none_without_credentials() {
let _ = GcpCredentials::from_env();
}
}