use {
anyhow::{
Context,
Result,
},
std::process::{
Command,
Stdio,
},
};
#[derive(Debug, Clone)]
pub struct GcpSecretSpec {
pub secret: String,
pub version: Option<String>,
}
pub struct GcpSecretManager;
impl GcpSecretManager {
pub fn new() -> Result<Self> {
Ok(Self)
}
pub fn access_secret(&self, spec: &GcpSecretSpec) -> Result<String> {
let version = spec.version.as_deref().unwrap_or("latest");
let (project, secret_name) = parse_project_and_secret(&spec.secret)
.context("Invalid GCP secret format. Expected 'projects/<project>/secrets/<name>'")?;
let mut cmd = Command::new("gcloud");
cmd.args(["secrets", "versions", "access", version, "--quiet"])
.arg("--secret")
.arg(&secret_name)
.arg("--project")
.arg(&project);
let output = cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.context("Failed to execute gcloud to access secret")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("gcloud failed: {}", stderr));
}
let value = String::from_utf8(output.stdout).context("Secret value is not valid UTF-8")?;
Ok(value.trim_end_matches(['\n', '\r']).to_string())
}
}
fn parse_project_and_secret(fqn: &str) -> Result<(String, String)> {
let parts: Vec<&str> = fqn.split('/').collect();
if parts.len() < 4 || parts[0] != "projects" || parts[2] != "secrets" {
return Err(anyhow::anyhow!("Invalid secret resource: {}", fqn));
}
let project = parts[1].to_string();
let secret_name = parts[3].to_string();
Ok((project, secret_name))
}