use crate::error::{ErrorData, Result};
use crate::traits::SecretPresence;
use alien_error::{Context, ContextError, IntoAlienError};
use alien_k8s_clients::secrets::SecretsApi;
use async_trait::async_trait;
use k8s_openapi::api::core::v1::Secret;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug)]
pub struct KubernetesSecretVault {
client: Arc<dyn SecretsApi>,
namespace: String,
vault_prefix: String,
}
impl KubernetesSecretVault {
pub fn new(client: Arc<dyn SecretsApi>, namespace: String, vault_prefix: String) -> Self {
Self {
client,
namespace,
vault_prefix,
}
}
fn secret_resource_name(&self, secret_name: &str) -> String {
alien_core::vault_naming::kubernetes_secret_name(&self.vault_prefix, secret_name)
}
}
#[async_trait]
impl crate::traits::Binding for KubernetesSecretVault {}
#[async_trait]
impl crate::traits::Vault for KubernetesSecretVault {
async fn secret_presence(&self, secret_name: &str) -> Result<SecretPresence> {
let secret_resource_name = self.secret_resource_name(secret_name);
match self
.client
.get_secret_metadata(&self.namespace, &secret_resource_name)
.await
{
Ok(_) => Ok(SecretPresence::Present),
Err(error)
if matches!(
error.error,
Some(alien_client_core::ErrorData::RemoteResourceNotFound { .. })
) =>
{
Ok(SecretPresence::Missing)
}
Err(error) => Err(error.context(ErrorData::CloudPlatformError {
message: format!(
"Failed to read Secret '{}' in namespace '{}'",
secret_resource_name, self.namespace
),
resource_id: None,
})),
}
}
async fn get_secret(&self, secret_name: &str) -> Result<String> {
let secret_resource_name = self.secret_resource_name(secret_name);
let secret = self
.client
.get_secret(&self.namespace, &secret_resource_name)
.await
.map_err(|error| {
super::secret_read_error(
error,
&self.vault_prefix,
secret_name,
format!("Failed to get secret '{}'", secret_name),
)
})?;
let value = secret
.data
.as_ref()
.and_then(|data| data.get("value"))
.ok_or_else(|| {
alien_error::AlienError::new(ErrorData::CloudPlatformError {
message: format!("Secret '{}' has no 'value' field", secret_name),
resource_id: None,
})
})?;
let decoded = String::from_utf8(value.0.clone())
.into_alien_error()
.context(ErrorData::CloudPlatformError {
message: format!("Failed to decode secret '{}' value", secret_name),
resource_id: None,
})?;
Ok(decoded)
}
async fn set_secret(&self, secret_name: &str, value: &str) -> Result<()> {
let secret_resource_name = self.secret_resource_name(secret_name);
let mut data = BTreeMap::new();
data.insert(
"value".to_string(),
k8s_openapi::ByteString(value.as_bytes().to_vec()),
);
let secret = Secret {
metadata: ObjectMeta {
name: Some(secret_resource_name.clone()),
namespace: Some(self.namespace.clone()),
labels: Some({
let mut labels = BTreeMap::new();
labels.insert("managed-by".to_string(), "operator".to_string());
labels.insert("vault-prefix".to_string(), self.vault_prefix.clone());
labels
}),
..Default::default()
},
data: Some(data),
..Default::default()
};
match self.client.create_secret(&self.namespace, &secret).await {
Ok(_) => Ok(()),
Err(e) => {
if matches!(
e.error,
Some(alien_client_core::ErrorData::RemoteResourceConflict { .. })
) {
self.client
.update_secret(&self.namespace, &secret_resource_name, &secret)
.await
.context(ErrorData::CloudPlatformError {
message: format!("Failed to update secret '{}'", secret_name),
resource_id: None,
})?;
Ok(())
} else {
Err(e.context(ErrorData::CloudPlatformError {
message: format!("Failed to create secret '{}'", secret_name),
resource_id: None,
}))
}
}
}
}
async fn delete_secret(&self, secret_name: &str) -> Result<()> {
let secret_resource_name = self.secret_resource_name(secret_name);
match self
.client
.delete_secret(&self.namespace, &secret_resource_name)
.await
{
Ok(()) => Ok(()),
Err(error)
if matches!(
error.error,
Some(alien_client_core::ErrorData::RemoteResourceNotFound { .. })
) =>
{
Ok(())
}
Err(error) => Err(error.context(ErrorData::CloudPlatformError {
message: format!("Failed to delete secret '{secret_name}'"),
resource_id: None,
})),
}
}
async fn list_secrets(&self) -> Result<Vec<String>> {
let label_selector = format!("managed-by=operator,vault-prefix={}", self.vault_prefix);
let list = self
.client
.list_secrets(&self.namespace, Some(label_selector), None)
.await
.context(ErrorData::CloudPlatformError {
message: format!(
"Failed to list secrets for vault prefix '{}'",
self.vault_prefix
),
resource_id: None,
})?;
let name_prefix = self.secret_resource_name("");
let names = list
.items
.into_iter()
.filter_map(|secret| secret.metadata.name)
.filter_map(|name| name.strip_prefix(&name_prefix).map(str::to_string))
.collect();
Ok(names)
}
}