use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::flex;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SecretDef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<String>,
#[serde(
default,
deserialize_with = "flex::bool",
skip_serializing_if = "std::ops::Not::not"
)]
#[schemars(with = "flex::BoolOrString")]
pub external: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub age: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refresh: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub on_change: Option<OnChange>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rotate: Option<Vec<String>>,
}
#[derive(
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
JsonSchema,
)]
#[serde(rename_all = "lowercase")]
pub enum OnChange {
None,
Restart,
#[default]
Roll,
}
impl OnChange {
pub fn as_str(self) -> &'static str {
match self {
OnChange::None => "none",
OnChange::Restart => "restart",
OnChange::Roll => "roll",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SecretAs {
#[default]
Env,
File,
}
impl std::fmt::Display for OnChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl SecretDef {
pub fn validate(&self) -> std::result::Result<(), String> {
let sources = [
self.file.is_some(),
self.environment.is_some(),
self.external,
self.age.is_some(),
self.driver.is_some(),
];
if sources.iter().filter(|s| **s).count() != 1 {
return Err(
"needs exactly one of file, environment, external, age, or driver (with name)"
.into(),
);
}
if self.name.is_some() && !self.external && self.driver.is_none() {
return Err("name goes with external or driver".into());
}
if self.driver.is_some() && self.name.as_deref().is_none_or(str::is_empty) {
return Err("driver needs name: the driver's reference to the secret".into());
}
if self.external {
if let Some(n) = &self.name {
crate::secrets::validate_name(n).map_err(|e| e.to_string())?;
}
}
if self.age.as_deref().is_some_and(|a| a.trim().is_empty()) {
return Err("age is empty".into());
}
if self
.rotate
.as_ref()
.is_some_and(|a| a.is_empty() || a[0].is_empty())
{
return Err("rotate needs a command: [argv...]".into());
}
if let Some(r) = &self.refresh {
if self.driver.is_none() {
return Err("refresh goes with driver".into());
}
let d = flex::parse_duration(r).map_err(|e| format!("refresh: {e}"))?;
if d < std::time::Duration::from_secs(10) {
return Err(format!("refresh {r:?}: at least 10s"));
}
}
Ok(())
}
pub fn store_name<'a>(&'a self, key: &'a str) -> Option<&'a str> {
self.external.then(|| self.name.as_deref().unwrap_or(key))
}
pub fn refresh_interval(&self) -> std::time::Duration {
self.refresh
.as_deref()
.and_then(|r| flex::parse_duration(r).ok())
.unwrap_or(DEFAULT_SECRET_REFRESH)
}
pub fn is_client_side(&self) -> bool {
self.file.is_some() || self.environment.is_some()
}
pub fn source_kind(&self) -> &'static str {
if self.file.is_some() {
"file"
} else if self.environment.is_some() {
"environment"
} else if self.external {
"external"
} else if self.age.is_some() {
"age"
} else if self.driver.is_some() {
"driver"
} else {
"none"
}
}
}
pub const DEFAULT_SECRET_REFRESH: std::time::Duration = std::time::Duration::from_secs(3600);