use bon::Builder;
use google_cloud_secretmanager_v1::client::SecretManagerService;
use huskarl_core::secrets::{Secret, SecretDecoder, SecretOutput};
use snafu::prelude::*;
use super::SecretVersion;
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum SecretVersionsError {
GetSecretVersion {
source: google_cloud_secretmanager_v1::Error,
},
ListSecretVersions {
source: google_cloud_secretmanager_v1::Error,
},
NoEnabledSecretVersions,
PrimaryVersionNotFound,
}
impl huskarl_core::Error for SecretVersionsError {
fn is_retryable(&self) -> bool {
match self {
Self::GetSecretVersion { source } | Self::ListSecretVersions { source } => {
source.is_exhausted() || source.is_timeout()
}
Self::NoEnabledSecretVersions | Self::PrimaryVersionNotFound => false,
}
}
}
pub struct ActiveSecretVersions<D: SecretDecoder> {
pub primary: SecretVersion<D>,
pub all: Vec<SecretVersion<D>>,
}
impl<D: SecretDecoder> ActiveSecretVersions<D> {
pub async fn get_all_values(
&self,
) -> Result<(SecretOutput<D::Output>, Vec<SecretOutput<D::Output>>), super::SecretError> {
futures_util::future::try_join(
self.primary.get_secret_value(),
futures_util::future::try_join_all(
self.all
.iter()
.map(huskarl_core::secrets::Secret::get_secret_value),
),
)
.await
}
}
#[derive(Debug, Clone, Builder)]
pub struct SecretVersions<D: SecretDecoder> {
decoder: D,
client: SecretManagerService,
#[builder(into)]
secret_name: String,
#[builder(into)]
primary_alias: String,
max_versions: Option<usize>,
}
impl<D: SecretDecoder> SecretVersions<D> {
pub fn get_primary_secret(&self) -> SecretVersion<D> {
SecretVersion::builder()
.decoder(self.decoder.clone())
.client(self.client.clone())
.resource_name(format!(
"{}/versions/{}",
self.secret_name, self.primary_alias
))
.build()
}
pub async fn get_secrets(&self) -> Result<Vec<SecretVersion<D>>, SecretVersionsError> {
let versions = self.list_enabled_versions().await?;
ensure!(!versions.is_empty(), NoEnabledSecretVersionsSnafu);
Ok(versions
.into_iter()
.map(|v| {
SecretVersion::builder()
.decoder(self.decoder.clone())
.client(self.client.clone())
.resource_name(v.name)
.build()
})
.collect())
}
pub async fn all(&self) -> Result<ActiveSecretVersions<D>, SecretVersionsError> {
let primary_alias_resource =
format!("{}/versions/{}", self.secret_name, self.primary_alias);
let (primary_meta, raw) = futures_util::try_join!(
async {
self.client
.get_secret_version()
.set_name(&primary_alias_resource)
.send()
.await
.context(GetSecretVersionSnafu)
},
self.list_enabled_versions(),
)?;
let primary_name = primary_meta.name;
ensure!(!raw.is_empty(), NoEnabledSecretVersionsSnafu);
let mut primary_opt: Option<SecretVersion<D>> = None;
let all: Vec<SecretVersion<D>> = raw
.into_iter()
.map(|v| {
let handle = SecretVersion::builder()
.decoder(self.decoder.clone())
.client(self.client.clone())
.resource_name(v.name.clone())
.build();
if v.name == primary_name {
primary_opt = Some(handle.clone());
}
handle
})
.collect();
let primary = primary_opt.ok_or_else(|| PrimaryVersionNotFoundSnafu.build())?;
Ok(ActiveSecretVersions { primary, all })
}
async fn list_enabled_versions(
&self,
) -> Result<Vec<google_cloud_secretmanager_v1::model::SecretVersion>, SecretVersionsError> {
let mut all_versions = Vec::new();
let mut page_token = String::new();
loop {
let remaining = self
.max_versions
.map(|m| m.saturating_sub(all_versions.len()));
if remaining == Some(0) {
break;
}
let mut request = self
.client
.list_secret_versions()
.set_parent(&self.secret_name)
.set_filter("state=ENABLED");
if let Some(n) = remaining {
request = request.set_page_size(i32::try_from(n).unwrap_or(i32::MAX));
}
if !page_token.is_empty() {
request = request.set_page_token(&page_token);
}
let response = request.send().await.context(ListSecretVersionsSnafu)?;
all_versions.extend(response.versions);
if response.next_page_token.is_empty()
|| self.max_versions.is_some_and(|m| all_versions.len() >= m)
{
break;
}
page_token = response.next_page_token;
}
Ok(all_versions)
}
}