use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use k8s_openapi::ByteString;
use k8s_openapi::api::core::v1::Secret;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use kube::api::{Api, PostParams};
use kube::{Client, ResourceExt};
use tracing::debug;
use crate::server::config::SecretsConfig;
use crate::server::error::AppError;
use super::{ServerSecrets, StoredSecrets};
fn format_kube_error(context: &str, err: kube::Error) -> AppError {
let mut msg = format!("{context}: {err}");
let mut source = std::error::Error::source(&err);
while let Some(cause) = source {
msg.push_str(&format!("\n caused by: {cause}"));
source = cause.source();
}
AppError::SecretStore(msg)
}
pub struct K8sSecretStore {
secret_name: String,
namespace: Option<String>,
secret_key: String,
}
impl K8sSecretStore {
pub fn new(secret_name: String, namespace: Option<String>, secret_key: String) -> Self {
Self {
secret_name,
namespace,
secret_key,
}
}
async fn api(&self) -> Result<Api<Secret>, AppError> {
let client = Client::try_default()
.await
.map_err(|e| format_kube_error("failed to initialise Kubernetes client", e))?;
let namespace = self
.namespace
.clone()
.unwrap_or_else(|| client.default_namespace().to_string());
Ok(Api::namespaced(client, &namespace))
}
async fn read_envelope(&self) -> Result<Option<StoredSecrets>, AppError> {
let api = self.api().await?;
let secret = api
.get_opt(&self.secret_name)
.await
.map_err(|e| format_kube_error("failed to read Kubernetes Secret", e))?;
let Some(secret) = secret else {
debug!(secret = %self.secret_name, "Kubernetes Secret not found");
return Ok(None);
};
let data = secret.data.unwrap_or_default();
let Some(ByteString(raw)) = data.get(&self.secret_key) else {
return Err(AppError::SecretStore(format!(
"Kubernetes Secret '{}' exists but has no '{}' key",
self.secret_name, self.secret_key
)));
};
let json = std::str::from_utf8(raw).map_err(|e| {
AppError::SecretStore(format!("Kubernetes Secret value is not valid UTF-8: {e}"))
})?;
let env = StoredSecrets::parse(json.trim()).map_err(|e| {
AppError::SecretStore(format!(
"failed to deserialize secrets from Kubernetes Secret: {e}"
))
})?;
Ok(Some(env))
}
async fn write_envelope(&self, env: &StoredSecrets) -> Result<(), AppError> {
let json = env.to_json().map_err(|e| {
AppError::Internal(format!("envelope serialization for Kubernetes: {e}"))
})?;
let api = self.api().await?;
match api
.get_opt(&self.secret_name)
.await
.map_err(|e| format_kube_error("failed to read Kubernetes Secret", e))?
{
Some(mut existing) => {
let mut data = existing.data.take().unwrap_or_default();
data.insert(self.secret_key.clone(), ByteString(json.into_bytes()));
existing.data = Some(data);
existing.string_data = None;
api.replace(&self.secret_name, &PostParams::default(), &existing)
.await
.map_err(|e| format_kube_error("failed to update Kubernetes Secret", e))?;
debug!(secret = %self.secret_name, "secrets stored in existing Kubernetes Secret");
Ok(())
}
None => {
let mut data = BTreeMap::new();
data.insert(self.secret_key.clone(), ByteString(json.into_bytes()));
let secret = Secret {
metadata: ObjectMeta {
name: Some(self.secret_name.clone()),
..Default::default()
},
data: Some(data),
type_: Some("Opaque".to_string()),
..Default::default()
};
let created = api
.create(&PostParams::default(), &secret)
.await
.map_err(|e| format_kube_error("failed to create Kubernetes Secret", e))?;
debug!(secret = %created.name_any(), "secrets created in Kubernetes Secret");
Ok(())
}
}
}
}
impl super::SecretStore for K8sSecretStore {
fn get(
&self,
) -> Pin<Box<dyn Future<Output = Result<Option<ServerSecrets>, AppError>> + Send + '_>> {
Box::pin(async {
let env = self.read_envelope().await?;
let secrets = env.and_then(|e| e.secrets);
if secrets.is_some() {
debug!(secret = %self.secret_name, "secrets loaded from Kubernetes Secret");
}
Ok(secrets)
})
}
fn set(
&self,
secrets: &ServerSecrets,
) -> Pin<Box<dyn Future<Output = Result<(), AppError>> + Send + '_>> {
let secrets = secrets.clone();
Box::pin(async move {
let mut env = self.read_envelope().await?.unwrap_or_default();
env.secrets = Some(secrets);
self.write_envelope(&env).await?;
debug!(secret = %self.secret_name, "secrets stored in Kubernetes Secret");
Ok(())
})
}
fn get_bootstrap_seed(
&self,
) -> Pin<Box<dyn Future<Output = Result<Option<[u8; 32]>, AppError>> + Send + '_>> {
Box::pin(async {
let env = self.read_envelope().await?;
match env.and_then(|e| e.bootstrap_seed) {
Some(b64) => {
let seed = StoredSecrets::decode_seed(&b64)?;
debug!(secret = %self.secret_name, "bootstrap seed loaded from Kubernetes Secret");
Ok(Some(seed))
}
None => Ok(None),
}
})
}
fn set_bootstrap_seed(
&self,
seed: &[u8; 32],
) -> Pin<Box<dyn Future<Output = Result<(), AppError>> + Send + '_>> {
let seed_owned = *seed;
Box::pin(async move {
let mut env = self.read_envelope().await?.unwrap_or_default();
env.bootstrap_seed = Some(StoredSecrets::encode_seed(&seed_owned));
self.write_envelope(&env).await?;
debug!(secret = %self.secret_name, "bootstrap seed stored in Kubernetes Secret");
Ok(())
})
}
fn clear_bootstrap_seed(
&self,
) -> Pin<Box<dyn Future<Output = Result<(), AppError>> + Send + '_>> {
Box::pin(async {
let Some(mut env) = self.read_envelope().await? else {
return Ok(());
};
if env.bootstrap_seed.is_none() {
return Ok(());
}
env.bootstrap_seed = None;
self.write_envelope(&env).await?;
debug!(secret = %self.secret_name, "bootstrap seed cleared from Kubernetes Secret");
Ok(())
})
}
}
pub fn from_config(secrets: &SecretsConfig) -> Result<K8sSecretStore, AppError> {
let secret_name = secrets.k8s_secret_name.clone().ok_or_else(|| {
AppError::Config("secrets.k8s_secret_name is required for the Kubernetes backend".into())
})?;
Ok(K8sSecretStore::new(
secret_name,
secrets.k8s_namespace.clone(),
secrets.k8s_secret_key.clone(),
))
}