use crate::config::parsers::polymorphic_vec;
use crate::path::{AbsolutePath, PathMapping};
use crate::secrets::{MemSize, Secret, SecretError};
use crate::write::{FileWriter, FileWriterArgs};
use clap::{Args, ValueEnum};
use locket_derive::LayeredConfig;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct SecretManagerConfig {
pub map: Vec<PathMapping>,
pub secrets: Vec<Secret>,
pub out: AbsolutePath,
pub inject_failure_policy: InjectFailurePolicy,
pub max_file_size: MemSize,
pub writer: FileWriter,
}
impl Default for SecretManagerConfig {
fn default() -> Self {
SecretManagerConfig {
map: Vec::new(),
secrets: Vec::new(),
#[cfg(target_os = "linux")]
out: AbsolutePath::new("/run/secrets/locket"),
#[cfg(target_os = "macos")]
out: AbsolutePath::new("/private/tmp/locket"),
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
out: AbsolutePath::new("./secrets"), inject_failure_policy: InjectFailurePolicy::default(),
max_file_size: MemSize::default(),
writer: FileWriter::default(),
}
}
}
impl SecretManagerConfig {
pub fn validate_structure(&mut self) -> Result<(), SecretError> {
let mut sources = Vec::new();
let mut destinations = Vec::new();
for m in &self.map {
sources.push(m.src());
destinations.push(m.dst());
}
destinations.push(&self.out);
for src in &sources {
for dst in &destinations {
if dst.starts_with(src) {
return Err(SecretError::Loop {
src: src.to_path_buf(),
dst: dst.to_path_buf(),
});
}
if src.starts_with(dst) {
return Err(SecretError::Destructive {
src: src.to_path_buf(),
dst: dst.to_path_buf(),
});
}
}
}
Ok(())
}
pub fn with_writer(mut self, writer: FileWriter) -> Self {
self.writer = writer;
self
}
pub fn with_outdir(mut self, outdir: AbsolutePath) -> Self {
self.out = outdir;
self
}
pub fn with_failure_policy(mut self, policy: InjectFailurePolicy) -> Self {
self.inject_failure_policy = policy;
self
}
pub fn with_secrets(mut self, secrets: Vec<Secret>) -> Self {
self.secrets = secrets;
self
}
pub fn with_mappings(mut self, mappings: Vec<PathMapping>) -> Self {
self.map = mappings;
self
}
}
#[derive(Copy, Clone, Debug, ValueEnum, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum InjectFailurePolicy {
Error,
#[default]
Passthrough,
Ignore,
}
impl std::fmt::Display for InjectFailurePolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_possible_value()
.expect("no values are skipped")
.get_name()
.fmt(f)
}
}
#[derive(Debug, Clone, Args, Deserialize, Serialize, LayeredConfig, Default)]
#[serde(rename_all = "kebab-case")]
#[locket(try_into = "SecretManagerConfig")]
pub struct SecretManagerArgs {
#[arg(
long,
alias = "secret_map",
env = "SECRET_MAP",
value_delimiter = ',',
hide_env_values = true
)]
#[serde(alias = "secret_map", default, deserialize_with = "polymorphic_vec")]
#[locket(docs = "
TOML syntax supports list of strings or map form:
List form:
map = [\"/templates:/run/secrets/app\", \"/config:/run/secrets/config\"]
Map form:
[map]
source = \"/templates\"
destination = \"/run/secrets/app\"
[map]
source = \"/config\"
destination = \"/run/secrets/config\"
")]
pub map: Vec<PathMapping>,
#[arg(
long,
alias = "secret",
env = "LOCKET_SECRETS",
value_name = "label={{template}}",
value_delimiter = ',',
hide_env_values = true
)]
#[serde(alias = "secret", deserialize_with = "polymorphic_vec", default)]
#[locket(docs = "
TOML syntax supports list of strings or map form:
List form:
secrets = [\"db_password={{..}}\", \"api_key={{..}}\"]
Map form:
[secrets]
db_password = \"{{..}}\"
api_key = \"{{..}}\"
")]
pub secrets: Vec<Secret>,
#[arg(long, env = "DEFAULT_SECRET_DIR")]
#[locket(default = SecretManagerConfig::default().out)]
pub out: Option<AbsolutePath>,
#[arg(long, env = "INJECT_POLICY", value_enum)]
#[locket(default = InjectFailurePolicy::Passthrough)]
pub inject_failure_policy: Option<InjectFailurePolicy>,
#[arg(long, env = "MAX_FILE_SIZE")]
#[locket(default = MemSize::default())]
pub max_file_size: Option<MemSize>,
#[command(flatten)]
#[serde(flatten)]
pub writer: FileWriterArgs,
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct TestWrapper {
#[serde(deserialize_with = "crate::config::parsers::polymorphic_vec")]
secrets: Vec<Secret>,
}
#[test]
fn test_list_syntax_named_and_anonymous() {
let toml_input = r#"
secrets = [
"name=template",
"."
]
"#;
let parsed: TestWrapper = toml::from_str(toml_input).expect("Should parse list");
assert_eq!(parsed.secrets.len(), 2);
let named = &parsed.secrets[0];
let anonymous = &parsed.secrets[1];
if let Secret::Named {
key: label,
source: _value,
} = named
{
assert_eq!(label.as_ref(), "name");
} else {
panic!("Expected first secret to be Named, but got: {:?}", named);
}
assert!(matches!(anonymous, Secret::Anonymous(_)));
}
#[test]
fn test_map_syntax_converts_to_named() {
let toml_input = r#"
[secrets]
key = "val"
"custom" = "val"
"#;
let parsed: TestWrapper = toml::from_str(toml_input).expect("Should parse map");
assert_eq!(parsed.secrets.len(), 2);
let debug_out = format!("{:?}", parsed.secrets);
assert!(debug_out.contains("key"));
assert!(debug_out.contains("custom"));
}
}