use std::env;
use std::fmt::Debug;
use reqwest::Client;
use tracing::debug;
use url::Url;
use super::jwt_bearer;
use crate::http::shared_client;
use crate::{CredentialResult, Token};
#[derive(Debug)]
pub struct KubernetesAuthProvider {
client: Client,
token_endpoint: Url,
workload_id: String,
}
impl KubernetesAuthProvider {
pub fn new(token_endpoint: Url, workload_id: String) -> Self {
KubernetesAuthProvider {
client: shared_client(),
token_endpoint,
workload_id,
}
}
fn configured_token_path() -> Option<String> {
env::var("SPIRAL_K8S_SA_TOKEN_PATH")
.ok()
.filter(|s| !s.is_empty())
}
fn read_subject_token_from(path: &str) -> Option<String> {
std::fs::read_to_string(path)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
}
impl KubernetesAuthProvider {
pub async fn token(&self) -> CredentialResult<Option<Token>> {
let Some(path) = Self::configured_token_path() else {
return Ok(None);
};
let Some(sa_token) = Self::read_subject_token_from(&path) else {
return Ok(None);
};
let token = jwt_bearer(
&self.client,
&self.token_endpoint,
self.workload_id.clone(),
sa_token,
None,
)
.await?;
debug!("KubernetesAuthProvider obtained a workload token");
Ok(Some(token))
}
}
impl_token_source!(KubernetesAuthProvider);
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
#[test]
fn test_configured_token_path() {
unsafe {
env::remove_var("SPIRAL_K8S_SA_TOKEN_PATH");
assert!(KubernetesAuthProvider::configured_token_path().is_none());
env::set_var("SPIRAL_K8S_SA_TOKEN_PATH", "");
assert!(KubernetesAuthProvider::configured_token_path().is_none());
env::set_var("SPIRAL_K8S_SA_TOKEN_PATH", "/custom/path/token");
assert_eq!(
KubernetesAuthProvider::configured_token_path().as_deref(),
Some("/custom/path/token")
);
env::remove_var("SPIRAL_K8S_SA_TOKEN_PATH");
}
}
#[test]
fn test_read_subject_token_absent_path_returns_none() {
assert!(
KubernetesAuthProvider::read_subject_token_from("/nonexistent/spiral/xyz").is_none()
);
}
#[test]
fn test_read_subject_token_reads_and_trims_file() {
let dir = std::env::temp_dir();
let path = dir.join("spiral_k8s_test_token");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(b" my-sa-token\n").unwrap();
let got = KubernetesAuthProvider::read_subject_token_from(path.to_str().unwrap());
assert_eq!(got.as_deref(), Some("my-sa-token"));
}
#[test]
fn test_read_subject_token_empty_file_returns_none() {
let dir = std::env::temp_dir();
let path = dir.join("spiral_k8s_test_token_empty");
std::fs::write(&path, b" \n").unwrap();
assert!(KubernetesAuthProvider::read_subject_token_from(path.to_str().unwrap()).is_none());
}
}