use crate::config::schema::GcpConfig;
use crate::credentials::cache::{CachedCredential, CredentialCache};
use crate::error::{MinoError, MinoResult};
use chrono::{Duration, Utc};
use std::process::Stdio;
use tokio::process::Command;
use tracing::debug;
pub struct GcpCredentials;
impl GcpCredentials {
const CACHE_KEY: &'static str = "gcp-token";
pub async fn get_access_token(
config: &GcpConfig,
cache: &CredentialCache,
) -> MinoResult<String> {
if let Some(cached) = cache.get(Self::CACHE_KEY).await? {
debug!("Using cached GCP access token");
return Ok(cached.value);
}
let token = Self::get_access_token_internal(config).await?;
let expires_at = Utc::now() + Duration::minutes(55);
let cached = CachedCredential::new("gcp", token.clone(), expires_at);
cache.set(Self::CACHE_KEY, &cached).await?;
Ok(token)
}
async fn get_access_token_internal(config: &GcpConfig) -> MinoResult<String> {
debug!("Requesting GCP access token...");
let mut cmd = Command::new("gcloud");
cmd.args(["auth", "print-access-token"]);
if let Some(account) = &config.service_account {
cmd.args(["--impersonate-service-account", account]);
}
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let output = cmd
.output()
.await
.map_err(|e| MinoError::command_failed("gcloud auth print-access-token", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not logged in") || stderr.contains("no active account") {
return Err(MinoError::GcpNotAuthenticated);
}
return Err(MinoError::GcpCredential(stderr.to_string()));
}
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
if token.is_empty() {
return Err(MinoError::GcpCredential("Empty token returned".to_string()));
}
Ok(token)
}
pub async fn is_authenticated() -> bool {
let result = Command::new("gcloud")
.args(["auth", "print-identity-token"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await;
result.map(|s| s.success()).unwrap_or(false)
}
pub async fn get_project() -> MinoResult<Option<String>> {
let output = Command::new("gcloud")
.args(["config", "get-value", "project"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.await
.map_err(|e| MinoError::command_failed("gcloud config get-value project", e))?;
if output.status.success() {
let project = String::from_utf8_lossy(&output.stdout).trim().to_string();
if project.is_empty() || project == "(unset)" {
Ok(None)
} else {
Ok(Some(project))
}
} else {
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn project_returns_option() {
let _ = GcpCredentials::get_project().await;
}
}