use std::path::{Path, PathBuf};
use std::process::Command;
use serde::{Deserialize, Serialize};
use crate::error::ShikumiError;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum SecretSource {
Backend(SecretBackend),
Literal(String),
}
impl SecretSource {
#[must_use]
pub const fn backend_kind(&self) -> SecretBackendKind {
match self {
Self::Literal(_) => SecretBackendKind::Literal,
Self::Backend(backend) => backend.kind(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SecretBackend {
Literal(String),
Command(String),
Op(String),
Sops(SopsRef),
Akeyless(String),
Vault(VaultRef),
AwsSecret(String),
GcpSecret(String),
}
impl SecretBackend {
#[must_use]
pub const fn kind(&self) -> SecretBackendKind {
match self {
Self::Literal(_) => SecretBackendKind::Literal,
Self::Command(_) => SecretBackendKind::Command,
Self::Op(_) => SecretBackendKind::Op,
Self::Sops(_) => SecretBackendKind::Sops,
Self::Akeyless(_) => SecretBackendKind::Akeyless,
Self::Vault(_) => SecretBackendKind::Vault,
Self::AwsSecret(_) => SecretBackendKind::AwsSecret,
Self::GcpSecret(_) => SecretBackendKind::GcpSecret,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SecretBackendKind {
Literal,
Command,
Op,
Sops,
Akeyless,
Vault,
AwsSecret,
GcpSecret,
}
impl SecretBackendKind {
pub const ALL: &'static [Self] = &[
Self::Literal,
Self::Command,
Self::Op,
Self::Sops,
Self::Akeyless,
Self::Vault,
Self::AwsSecret,
Self::GcpSecret,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Literal => "literal",
Self::Command => "command",
Self::Op => "op",
Self::Sops => "sops",
Self::Akeyless => "akeyless",
Self::Vault => "vault",
Self::AwsSecret => "aws_secret",
Self::GcpSecret => "gcp_secret",
}
}
}
impl crate::ClosedAxis for SecretBackendKind {
const ALL: &'static [Self] = Self::ALL;
}
impl crate::ClosedAxisLabel for SecretBackendKind {
fn as_str(self) -> &'static str {
Self::as_str(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SopsRef {
File(PathBuf),
Field { file: PathBuf, field: String },
}
impl SopsRef {
#[must_use]
pub const fn shape(&self) -> SecretRefShape {
match self {
Self::File(_) => SecretRefShape::Whole,
Self::Field { .. } => SecretRefShape::Field,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VaultRef {
Path(String),
Field { path: String, field: String },
}
impl VaultRef {
#[must_use]
pub const fn shape(&self) -> SecretRefShape {
match self {
Self::Path(_) => SecretRefShape::Whole,
Self::Field { .. } => SecretRefShape::Field,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SecretRefShape {
Whole,
Field,
}
impl SecretRefShape {
pub const ALL: &'static [Self] = &[Self::Whole, Self::Field];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Whole => "whole",
Self::Field => "field",
}
}
}
impl crate::ClosedAxis for SecretRefShape {
const ALL: &'static [Self] = Self::ALL;
}
impl crate::ClosedAxisLabel for SecretRefShape {
fn as_str(self) -> &'static str {
Self::as_str(self)
}
}
pub fn resolve(source: &SecretSource) -> Result<String, ShikumiError> {
match source {
SecretSource::Literal(value) => Ok(value.clone()),
SecretSource::Backend(SecretBackend::Literal(value)) => Ok(value.clone()),
SecretSource::Backend(SecretBackend::Command(cmd)) => resolve_command(cmd),
SecretSource::Backend(SecretBackend::Op(reference)) => resolve_op(reference),
SecretSource::Backend(SecretBackend::Sops(SopsRef::File(path))) => resolve_sops_file(path),
SecretSource::Backend(SecretBackend::Sops(SopsRef::Field { file, field })) => {
resolve_sops_field(file, field)
}
SecretSource::Backend(SecretBackend::Akeyless(name)) => resolve_akeyless(name),
SecretSource::Backend(SecretBackend::Vault(VaultRef::Path(path))) => {
resolve_vault(path, "value")
}
SecretSource::Backend(SecretBackend::Vault(VaultRef::Field { path, field })) => {
resolve_vault(path, field)
}
SecretSource::Backend(SecretBackend::AwsSecret(secret_id)) => resolve_aws_secret(secret_id),
SecretSource::Backend(SecretBackend::GcpSecret(name)) => resolve_gcp_secret(name),
}
}
pub fn resolve_command(cmd: &str) -> Result<String, ShikumiError> {
let output = Command::new("sh").arg("-c").arg(cmd).output()?;
capture_stdout(cmd, &output)
}
pub fn resolve_op(reference: &str) -> Result<String, ShikumiError> {
let output = Command::new("op").arg("read").arg(reference).output()?;
capture_stdout(&format!("op read {reference}"), &output)
}
pub fn resolve_sops_file(path: &Path) -> Result<String, ShikumiError> {
let output = Command::new("sops").arg("--decrypt").arg(path).output()?;
capture_stdout(&format!("sops --decrypt {}", path.display()), &output)
}
pub fn resolve_sops_field(path: &Path, field: &str) -> Result<String, ShikumiError> {
let cmd = format!(
"sops --decrypt {} | jq -r {}",
shell_escape(&path.display().to_string()),
shell_escape(field),
);
let value = resolve_command(&cmd)?;
if value == "null" {
return Err(ShikumiError::Parse(format!(
"sops field {field:?} in {} is null — check the field path",
path.display()
)));
}
Ok(value)
}
pub fn resolve_akeyless(name: &str) -> Result<String, ShikumiError> {
let output = Command::new("akeyless")
.args(["get-secret-value", "--name"])
.arg(name)
.output()?;
capture_stdout(&format!("akeyless get-secret-value --name {name}"), &output)
}
pub fn resolve_vault(path: &str, field: &str) -> Result<String, ShikumiError> {
let output = Command::new("vault")
.arg("read")
.arg(format!("-field={field}"))
.arg(path)
.output()?;
capture_stdout(&format!("vault read -field={field} {path}"), &output)
}
pub fn resolve_aws_secret(secret_id: &str) -> Result<String, ShikumiError> {
let output = Command::new("aws")
.args(["secretsmanager", "get-secret-value", "--secret-id"])
.arg(secret_id)
.args(["--query", "SecretString", "--output", "text"])
.output()?;
capture_stdout(
&format!("aws secretsmanager get-secret-value --secret-id {secret_id}"),
&output,
)
}
pub fn resolve_gcp_secret(name: &str) -> Result<String, ShikumiError> {
let (secret_path, version) = if let Some(idx) = name.find("/versions/") {
let (head, tail) = name.split_at(idx);
(head, &tail["/versions/".len()..])
} else {
(name, "latest")
};
let short_name = secret_path
.rsplit("/secrets/")
.next()
.unwrap_or(secret_path)
.trim_start_matches("secrets/");
let output = Command::new("gcloud")
.args(["secrets", "versions", "access"])
.arg(version)
.arg(format!("--secret={short_name}"))
.output()?;
capture_stdout(
&format!("gcloud secrets versions access {version} --secret={short_name}"),
&output,
)
}
#[cfg(feature = "akeyless-native")]
#[derive(Debug, Clone)]
pub struct AkeylessAuth {
pub gateway_url: String,
pub token: String,
}
#[cfg(feature = "akeyless-native")]
impl AkeylessAuth {
pub fn from_env() -> Result<Self, ShikumiError> {
let token = std::env::var("AKEYLESS_TOKEN").map_err(|_| {
ShikumiError::Parse(
"AKEYLESS_TOKEN not set — required for native Akeyless client".into(),
)
})?;
let gateway_url = std::env::var("AKEYLESS_GATEWAY_URL")
.unwrap_or_else(|_| "https://api.akeyless.io".into());
Ok(Self { gateway_url, token })
}
#[must_use]
pub fn configuration(&self) -> akeyless_api::apis::configuration::Configuration {
let mut cfg = akeyless_api::apis::configuration::Configuration::new();
cfg.base_path = self.gateway_url.clone();
cfg
}
}
#[cfg(feature = "akeyless-native")]
pub async fn resolve_akeyless_native(
auth: &AkeylessAuth,
name: &str,
) -> Result<String, ShikumiError> {
let cfg = auth.configuration();
let request = akeyless_api::models::GetSecretValue {
names: vec![name.to_string()],
token: Some(auth.token.clone()),
..Default::default()
};
let response = akeyless_api::apis::v2_api::get_secret_value(&cfg, request)
.await
.map_err(|e| {
ShikumiError::Parse(format!("akeyless get_secret_value({name}) failed: {e}"))
})?;
let obj = response.as_object().ok_or_else(|| {
ShikumiError::Parse(format!(
"akeyless response for {name} was not a JSON object: {response}"
))
})?;
let value = obj.get(name).ok_or_else(|| {
ShikumiError::Parse(format!(
"akeyless response missing key {name:?}: {response}"
))
})?;
value.as_str().map(|s| s.to_owned()).ok_or_else(|| {
ShikumiError::Parse(format!(
"akeyless value for {name} was not a string: {value}"
))
})
}
#[cfg(feature = "akeyless-native")]
pub async fn resolve_akeyless_auto(
auth: Option<&AkeylessAuth>,
name: &str,
) -> Result<String, ShikumiError> {
if let Some(a) = auth {
resolve_akeyless_native(a, name).await
} else {
resolve_akeyless(name)
}
}
#[cfg(feature = "aws-native")]
pub async fn resolve_aws_secret_native(
client: &aws_sdk_secretsmanager::Client,
secret_id: &str,
) -> Result<String, ShikumiError> {
let response = client
.get_secret_value()
.secret_id(secret_id)
.send()
.await
.map_err(|e| {
ShikumiError::Parse(format!(
"aws secretsmanager get-secret-value({secret_id}) failed: {e}"
))
})?;
response.secret_string().map(str::to_owned).ok_or_else(|| {
ShikumiError::Parse(format!(
"aws secret {secret_id} has no SecretString (binary secrets not supported here — use the SDK directly)"
))
})
}
#[cfg(feature = "aws-native")]
pub async fn aws_secretsmanager_client() -> aws_sdk_secretsmanager::Client {
let cfg = aws_config::load_from_env().await;
aws_sdk_secretsmanager::Client::new(&cfg)
}
#[cfg(feature = "aws-native")]
pub async fn resolve_aws_secret_auto(
client: Option<&aws_sdk_secretsmanager::Client>,
secret_id: &str,
) -> Result<String, ShikumiError> {
if let Some(c) = client {
resolve_aws_secret_native(c, secret_id).await
} else {
resolve_aws_secret(secret_id)
}
}
pub fn resolve_or_command(
literal: Option<&str>,
command: Option<&str>,
missing_field_name: &str,
) -> Result<String, ShikumiError> {
if let Some(value) = literal {
return Ok(value.to_owned());
}
if let Some(cmd) = command {
return resolve_command(cmd);
}
Err(ShikumiError::Parse(format!(
"secret {missing_field_name} not provided (set {missing_field_name} or {missing_field_name}_command)"
)))
}
fn capture_stdout(label: &str, output: &std::process::Output) -> Result<String, ShikumiError> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ShikumiError::Parse(format!(
"secret command {label:?} exited with {}: {}",
output.status,
stderr.trim()
)));
}
let stdout = String::from_utf8(output.stdout.clone())
.map_err(|e| ShikumiError::Parse(format!("secret command stdout not utf-8: {e}")))?;
Ok(stdout.trim_end().to_owned())
}
fn shell_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for c in s.chars() {
if c == '\'' {
out.push_str("'\\''");
} else {
out.push(c);
}
}
out.push('\'');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_echo_returns_stdout() {
let value = resolve_command("echo hunter2").unwrap();
assert_eq!(value, "hunter2");
}
#[test]
fn resolve_trims_trailing_newline() {
let value = resolve_command("printf 'secret\\n'").unwrap();
assert_eq!(value, "secret");
}
#[test]
fn resolve_preserves_leading_whitespace() {
let value = resolve_command("printf ' hello'").unwrap();
assert_eq!(value, " hello");
}
#[test]
fn resolve_multiline_stdout() {
let value = resolve_command("printf 'line1\\nline2\\n'").unwrap();
assert_eq!(value, "line1\nline2");
}
#[test]
fn resolve_command_failure_surfaces_stderr() {
let err = resolve_command("echo oops >&2; exit 17").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("oops"), "stderr should appear in error: {msg}");
assert!(
msg.contains("17") || msg.contains("exit"),
"exit status in error: {msg}"
);
}
#[test]
fn resolve_command_failure_is_parse_variant() {
let err = resolve_command("exit 1").unwrap_err();
assert!(err.is_parse(), "failed command should map to Parse variant");
}
#[test]
fn resolve_nonexistent_command_fails() {
let err = resolve_command("nonexistent-command-zzz-xyzzy").unwrap_err();
assert!(err.is_parse());
}
#[test]
fn resolve_empty_command_succeeds_empty_stdout() {
let value = resolve_command(":").unwrap();
assert_eq!(value, "");
}
#[test]
fn resolve_or_command_prefers_literal() {
let value = resolve_or_command(Some("plain"), Some("echo ignored"), "jwt_secret").unwrap();
assert_eq!(value, "plain");
}
#[test]
fn resolve_or_command_falls_back_to_command() {
let value = resolve_or_command(None, Some("echo from-cmd"), "jwt_secret").unwrap();
assert_eq!(value, "from-cmd");
}
#[test]
fn resolve_or_command_errors_when_neither_set() {
let err = resolve_or_command(None, None, "jwt_secret").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("jwt_secret"),
"error should name the missing field"
);
assert!(
msg.contains("jwt_secret_command"),
"error should suggest the _command fallback"
);
}
#[test]
fn resolve_or_command_propagates_command_error() {
let err = resolve_or_command(None, Some("exit 1"), "api_key").unwrap_err();
assert!(err.is_parse());
}
#[test]
fn resolve_command_with_shell_features() {
let value = resolve_command("echo abc | tr a-z A-Z").unwrap();
assert_eq!(value, "ABC");
}
#[test]
fn secret_source_parses_bare_string_as_literal() {
let source: SecretSource = serde_yaml::from_str("dev-secret").unwrap();
match source {
SecretSource::Literal(s) => assert_eq!(s, "dev-secret"),
other => panic!("expected Literal, got {other:?}"),
}
}
#[test]
fn secret_source_parses_op_reference() {
let source: SecretSource = serde_yaml::from_str("op: op://vault/item/field").unwrap();
match source {
SecretSource::Backend(SecretBackend::Op(r)) => {
assert_eq!(r, "op://vault/item/field");
}
other => panic!("expected Op, got {other:?}"),
}
}
#[test]
fn secret_source_parses_command() {
let source: SecretSource = serde_yaml::from_str("command: cat /tmp/secret").unwrap();
match source {
SecretSource::Backend(SecretBackend::Command(c)) => {
assert_eq!(c, "cat /tmp/secret");
}
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn secret_source_parses_akeyless() {
let source: SecretSource = serde_yaml::from_str("akeyless: /prod/jwt").unwrap();
match source {
SecretSource::Backend(SecretBackend::Akeyless(n)) => {
assert_eq!(n, "/prod/jwt");
}
other => panic!("expected Akeyless, got {other:?}"),
}
}
#[test]
fn secret_source_parses_sops_file_shorthand() {
let source: SecretSource = serde_yaml::from_str("sops: secrets/prod.yaml").unwrap();
match source {
SecretSource::Backend(SecretBackend::Sops(SopsRef::File(p))) => {
assert_eq!(p.to_str().unwrap(), "secrets/prod.yaml");
}
other => panic!("expected Sops File, got {other:?}"),
}
}
#[test]
fn secret_source_parses_sops_with_field() {
let yaml = "sops:\n file: secrets/prod.yaml\n field: jwt_secret";
let source: SecretSource = serde_yaml::from_str(yaml).unwrap();
match source {
SecretSource::Backend(SecretBackend::Sops(SopsRef::Field { file, field })) => {
assert_eq!(file.to_str().unwrap(), "secrets/prod.yaml");
assert_eq!(field, "jwt_secret");
}
other => panic!("expected Sops Field, got {other:?}"),
}
}
#[test]
fn secret_source_parses_explicit_literal() {
let source: SecretSource = serde_yaml::from_str("literal: dev-secret").unwrap();
let resolved = resolve(&source).unwrap();
assert!(
resolved == "dev-secret" || resolved.is_empty(),
"unexpected resolution: {resolved:?}"
);
}
#[test]
fn resolve_dispatches_literal() {
let value = resolve(&SecretSource::Literal("plain".into())).unwrap();
assert_eq!(value, "plain");
}
#[test]
fn resolve_dispatches_command() {
let source = SecretSource::Backend(SecretBackend::Command("echo dispatched".into()));
let value = resolve(&source).unwrap();
assert_eq!(value, "dispatched");
}
#[test]
fn resolve_dispatches_explicit_literal() {
let source = SecretSource::Backend(SecretBackend::Literal("explicit".into()));
let value = resolve(&source).unwrap();
assert_eq!(value, "explicit");
}
#[test]
fn shell_escape_plain_string() {
assert_eq!(shell_escape("hello"), "'hello'");
}
#[test]
fn shell_escape_single_quote() {
assert_eq!(shell_escape("it's"), "'it'\\''s'");
}
#[test]
fn shell_escape_preserves_spaces() {
assert_eq!(shell_escape("with space"), "'with space'");
}
#[test]
fn shell_escape_with_dollar() {
assert_eq!(shell_escape("$HOME"), "'$HOME'");
}
#[test]
fn shell_escape_roundtrips_through_sh() {
let inputs = ["hello", "it's", "with space", "$HOME", "back\\slash"];
for s in inputs {
let cmd = format!("printf %s {}", shell_escape(s));
let value = resolve_command(&cmd).unwrap();
assert_eq!(value, s, "round-trip failed for {s:?}");
}
}
#[test]
fn resolve_op_missing_cli_surfaces_error() {
let result = resolve_op("op://nonexistent-vault-zzz/nothing/here");
assert!(result.is_err(), "unknown op reference should error");
}
#[test]
fn resolve_sops_file_missing_path_errors() {
let result = resolve_sops_file(Path::new("/nonexistent/sops/file.yaml"));
assert!(result.is_err());
}
#[test]
fn resolve_sops_field_null_is_rejected() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), "").unwrap();
let value = resolve_command("echo null").unwrap();
assert_eq!(value, "null");
}
#[test]
fn resolve_akeyless_missing_cli_or_secret_errors() {
let result = resolve_akeyless("/shikumi-test/nonexistent-secret");
assert!(result.is_err(), "unknown akeyless secret should error");
}
#[test]
fn secret_source_parses_vault_bare_path() {
let source: SecretSource = serde_yaml::from_str("vault: secret/data/prod/app").unwrap();
match source {
SecretSource::Backend(SecretBackend::Vault(VaultRef::Path(p))) => {
assert_eq!(p, "secret/data/prod/app");
}
other => panic!("expected Vault Path, got {other:?}"),
}
}
#[test]
fn secret_source_parses_vault_with_field() {
let yaml = "vault:\n path: secret/data/prod/app\n field: password";
let source: SecretSource = serde_yaml::from_str(yaml).unwrap();
match source {
SecretSource::Backend(SecretBackend::Vault(VaultRef::Field { path, field })) => {
assert_eq!(path, "secret/data/prod/app");
assert_eq!(field, "password");
}
other => panic!("expected Vault Field, got {other:?}"),
}
}
#[test]
fn secret_source_parses_aws_secret() {
let source: SecretSource = serde_yaml::from_str("aws_secret: prod/hanabi/jwt").unwrap();
match source {
SecretSource::Backend(SecretBackend::AwsSecret(id)) => {
assert_eq!(id, "prod/hanabi/jwt");
}
other => panic!("expected AwsSecret, got {other:?}"),
}
}
#[test]
fn secret_source_parses_gcp_secret() {
let source: SecretSource =
serde_yaml::from_str("gcp_secret: projects/my-proj/secrets/jwt").unwrap();
match source {
SecretSource::Backend(SecretBackend::GcpSecret(name)) => {
assert_eq!(name, "projects/my-proj/secrets/jwt");
}
other => panic!("expected GcpSecret, got {other:?}"),
}
}
#[test]
fn resolve_vault_missing_cli_errors() {
let result = resolve_vault("secret/nonexistent", "value");
assert!(result.is_err(), "unknown vault path should error");
}
#[test]
fn resolve_aws_secret_missing_cli_errors() {
let result = resolve_aws_secret("shikumi-test/nonexistent-secret");
assert!(result.is_err(), "unknown AWS secret should error");
}
#[test]
fn resolve_gcp_secret_missing_cli_errors() {
let result = resolve_gcp_secret("projects/shikumi-test/secrets/nonexistent");
assert!(result.is_err(), "unknown GCP secret should error");
}
#[test]
fn gcp_secret_short_form_uses_latest_version() {
let name = "projects/my-proj/secrets/jwt";
let (secret_path, version) = if let Some(idx) = name.find("/versions/") {
let (head, tail) = name.split_at(idx);
(head, &tail["/versions/".len()..])
} else {
(name, "latest")
};
assert_eq!(secret_path, "projects/my-proj/secrets/jwt");
assert_eq!(version, "latest");
}
#[test]
fn gcp_secret_full_form_extracts_version() {
let name = "projects/my-proj/secrets/jwt/versions/3";
let (secret_path, version) = if let Some(idx) = name.find("/versions/") {
let (head, tail) = name.split_at(idx);
(head, &tail["/versions/".len()..])
} else {
(name, "latest")
};
assert_eq!(secret_path, "projects/my-proj/secrets/jwt");
assert_eq!(version, "3");
}
#[test]
fn resolve_dispatches_vault_missing_cli() {
let source = SecretSource::Backend(SecretBackend::Vault(VaultRef::Path(
"secret/nonexistent-shikumi-test".into(),
)));
let result = resolve(&source);
assert!(result.is_err());
}
#[test]
fn resolve_dispatches_aws_missing_cli() {
let source =
SecretSource::Backend(SecretBackend::AwsSecret("shikumi-test-nonexistent".into()));
let result = resolve(&source);
assert!(result.is_err());
}
#[test]
fn resolve_dispatches_gcp_missing_cli() {
let source = SecretSource::Backend(SecretBackend::GcpSecret(
"projects/shikumi/secrets/nonexistent".into(),
));
let result = resolve(&source);
assert!(result.is_err());
}
fn canonical_secret_backend_kind_samples() -> Vec<(SecretBackend, SecretBackendKind)> {
vec![
(
SecretBackend::Literal("dev".into()),
SecretBackendKind::Literal,
),
(
SecretBackend::Command("echo hunter2".into()),
SecretBackendKind::Command,
),
(
SecretBackend::Op("op://prod/app/jwt".into()),
SecretBackendKind::Op,
),
(
SecretBackend::Sops(SopsRef::File(PathBuf::from("secrets/prod.yaml"))),
SecretBackendKind::Sops,
),
(
SecretBackend::Sops(SopsRef::Field {
file: PathBuf::from("secrets/prod.yaml"),
field: "jwt_secret".into(),
}),
SecretBackendKind::Sops,
),
(
SecretBackend::Akeyless("/prod/my-secret".into()),
SecretBackendKind::Akeyless,
),
(
SecretBackend::Vault(VaultRef::Path("secret/data/prod/app".into())),
SecretBackendKind::Vault,
),
(
SecretBackend::Vault(VaultRef::Field {
path: "secret/data/prod/app".into(),
field: "password".into(),
}),
SecretBackendKind::Vault,
),
(
SecretBackend::AwsSecret("prod/app/jwt".into()),
SecretBackendKind::AwsSecret,
),
(
SecretBackend::GcpSecret("projects/p/secrets/jwt".into()),
SecretBackendKind::GcpSecret,
),
]
}
#[test]
fn secret_backend_kind_classifies_each_variant() {
for (backend, expected) in canonical_secret_backend_kind_samples() {
assert_eq!(
backend.kind(),
expected,
"SecretBackend::kind must classify {backend:?} as {expected:?}",
);
}
}
#[test]
fn secret_backend_kind_is_data_free() {
for literal in ["", "a", "very-long-secret-payload-with-special-chars-$@!"] {
assert_eq!(
SecretBackend::Literal(literal.into()).kind(),
SecretBackendKind::Literal,
);
}
for sops in [
SopsRef::File(PathBuf::from("a.yaml")),
SopsRef::File(PathBuf::from("/very/long/path/to/b.json")),
SopsRef::Field {
file: PathBuf::from("c.yaml"),
field: "k".into(),
},
] {
assert_eq!(SecretBackend::Sops(sops).kind(), SecretBackendKind::Sops);
}
for vault in [
VaultRef::Path("p".into()),
VaultRef::Field {
path: "p".into(),
field: "f".into(),
},
] {
assert_eq!(SecretBackend::Vault(vault).kind(), SecretBackendKind::Vault);
}
}
#[test]
fn secret_backend_kind_is_static_and_copy_and_hashable() {
fn assert_static<T: 'static>() {}
use std::collections::HashSet;
let mut set: HashSet<SecretBackendKind> = SecretBackendKind::ALL.iter().copied().collect();
set.insert(SecretBackendKind::Vault); assert_eq!(set.len(), SecretBackendKind::ALL.len());
let k = SecretBackendKind::Op;
let k2 = k;
let k3 = k;
assert_eq!(k, k2);
assert_eq!(k2, k3);
assert_static::<SecretBackendKind>();
}
#[test]
fn secret_backend_kind_all_has_no_duplicates() {
use std::collections::HashSet;
let set: HashSet<SecretBackendKind> = SecretBackendKind::ALL.iter().copied().collect();
assert_eq!(
set.len(),
SecretBackendKind::ALL.len(),
"SecretBackendKind::ALL must contain no duplicates; got: {:?}",
SecretBackendKind::ALL,
);
}
#[test]
fn secret_backend_kind_all_covers_every_constructible_backend() {
use std::collections::HashSet;
let declared: HashSet<SecretBackendKind> = SecretBackendKind::ALL.iter().copied().collect();
let observed: HashSet<SecretBackendKind> = canonical_secret_backend_kind_samples()
.iter()
.map(|(backend, _)| backend.kind())
.collect();
assert!(
observed.is_subset(&declared),
"SecretBackend::kind image must lie in SecretBackendKind::ALL; \
observed: {observed:?}, declared: {declared:?}",
);
}
#[test]
fn secret_backend_kind_all_equals_backend_kind_image() {
use std::collections::HashSet;
let declared: HashSet<SecretBackendKind> = SecretBackendKind::ALL.iter().copied().collect();
let observed: HashSet<SecretBackendKind> = canonical_secret_backend_kind_samples()
.iter()
.map(|(backend, _)| backend.kind())
.collect();
assert_eq!(
observed, declared,
"SecretBackend::kind image must equal SecretBackendKind::ALL",
);
}
#[test]
fn secret_backend_kind_all_declaration_order_matches_secret_backend() {
assert_eq!(
SecretBackendKind::ALL,
&[
SecretBackendKind::Literal,
SecretBackendKind::Command,
SecretBackendKind::Op,
SecretBackendKind::Sops,
SecretBackendKind::Akeyless,
SecretBackendKind::Vault,
SecretBackendKind::AwsSecret,
SecretBackendKind::GcpSecret,
],
);
}
#[test]
fn secret_backend_kind_as_str_yields_canonical_snake_case_names() {
assert_eq!(SecretBackendKind::Literal.as_str(), "literal");
assert_eq!(SecretBackendKind::Command.as_str(), "command");
assert_eq!(SecretBackendKind::Op.as_str(), "op");
assert_eq!(SecretBackendKind::Sops.as_str(), "sops");
assert_eq!(SecretBackendKind::Akeyless.as_str(), "akeyless");
assert_eq!(SecretBackendKind::Vault.as_str(), "vault");
assert_eq!(SecretBackendKind::AwsSecret.as_str(), "aws_secret");
assert_eq!(SecretBackendKind::GcpSecret.as_str(), "gcp_secret");
}
#[test]
fn secret_backend_kind_as_str_matches_serde_json_tag_for_each_variant() {
for (backend, expected_kind) in canonical_secret_backend_kind_samples() {
let value: serde_json::Value = serde_json::to_value(&backend).unwrap();
let object = value
.as_object()
.expect("externally-tagged SecretBackend serializes as a single-key object");
assert_eq!(
object.len(),
1,
"externally-tagged SecretBackend must serialize as exactly one key",
);
let key = object.keys().next().unwrap();
assert_eq!(
key,
expected_kind.as_str(),
"serde external tag for {backend:?} ({key:?}) must equal \
SecretBackendKind::as_str ({:?})",
expected_kind.as_str(),
);
}
}
#[test]
fn secret_backend_kind_from_canonical_str_round_trips_through_trait() {
use crate::ClosedAxisLabel;
for k in SecretBackendKind::ALL.iter().copied() {
assert_eq!(
<SecretBackendKind as ClosedAxisLabel>::from_canonical_str(k.as_str()),
Some(k),
"trait from_canonical_str must round-trip for {k:?}",
);
}
assert_eq!(
<SecretBackendKind as ClosedAxisLabel>::from_canonical_str("LITERAL"),
Some(SecretBackendKind::Literal),
);
assert_eq!(
<SecretBackendKind as ClosedAxisLabel>::from_canonical_str("Aws_Secret"),
Some(SecretBackendKind::AwsSecret),
);
assert_eq!(
<SecretBackendKind as ClosedAxisLabel>::from_canonical_str("aws"),
None,
);
assert_eq!(
<SecretBackendKind as ClosedAxisLabel>::from_canonical_str("op "),
None,
);
assert_eq!(
<SecretBackendKind as ClosedAxisLabel>::from_canonical_str(""),
None,
);
}
#[test]
fn secret_backend_kind_resolve_dispatch_arms_partition_by_kind() {
use std::collections::HashSet;
let mut witnessed: HashSet<SecretBackendKind> = HashSet::new();
for (backend, expected_kind) in canonical_secret_backend_kind_samples() {
let source = SecretSource::Backend(backend.clone());
let result = resolve(&source);
match expected_kind {
SecretBackendKind::Literal => {
assert!(result.is_ok(), "Literal must resolve to Ok");
}
_ => {
let _ = result;
}
}
witnessed.insert(backend.kind());
}
let declared: HashSet<SecretBackendKind> = SecretBackendKind::ALL.iter().copied().collect();
assert_eq!(
witnessed, declared,
"every SecretBackendKind variant must be witnessed by \
a canonical-sample backend reaching the resolve dispatch",
);
}
#[test]
fn secret_source_backend_kind_pins_known_sources() {
let cases: Vec<(SecretSource, SecretBackendKind)> = vec![
(
SecretSource::Literal("bare".into()),
SecretBackendKind::Literal,
),
(
SecretSource::Backend(SecretBackend::Literal("explicit".into())),
SecretBackendKind::Literal,
),
(
SecretSource::Backend(SecretBackend::Command("echo s".into())),
SecretBackendKind::Command,
),
(
SecretSource::Backend(SecretBackend::Op("op://v/i/f".into())),
SecretBackendKind::Op,
),
(
SecretSource::Backend(SecretBackend::Sops(SopsRef::File(PathBuf::from("s.yaml")))),
SecretBackendKind::Sops,
),
(
SecretSource::Backend(SecretBackend::Akeyless("/p/s".into())),
SecretBackendKind::Akeyless,
),
(
SecretSource::Backend(SecretBackend::Vault(VaultRef::Path("secret/p".into()))),
SecretBackendKind::Vault,
),
(
SecretSource::Backend(SecretBackend::AwsSecret("p/s".into())),
SecretBackendKind::AwsSecret,
),
(
SecretSource::Backend(SecretBackend::GcpSecret("projects/p/secrets/s".into())),
SecretBackendKind::GcpSecret,
),
];
for (source, expected) in cases {
assert_eq!(
source.backend_kind(),
expected,
"SecretSource::backend_kind must classify {source:?} as {expected:?}",
);
}
}
#[test]
fn secret_source_backend_kind_collapses_literal_paths() {
for payload in ["", "dev", "very-long-secret-payload-$@!"] {
let bare = SecretSource::Literal(payload.into());
let tagged = SecretSource::Backend(SecretBackend::Literal(payload.into()));
assert_eq!(bare.backend_kind(), SecretBackendKind::Literal);
assert_eq!(tagged.backend_kind(), SecretBackendKind::Literal);
assert_eq!(bare.backend_kind(), tagged.backend_kind());
}
}
#[test]
fn secret_source_backend_kind_wraps_secret_backend_kind_on_backend_variant() {
for (backend, expected) in canonical_secret_backend_kind_samples() {
let source = SecretSource::Backend(backend.clone());
assert_eq!(
source.backend_kind(),
backend.kind(),
"SecretSource::Backend(b).backend_kind() must equal b.kind() for {backend:?}",
);
assert_eq!(source.backend_kind(), expected);
}
}
#[test]
fn secret_source_backend_kind_image_lies_in_secret_backend_kind_all() {
use std::collections::HashSet;
let declared: HashSet<SecretBackendKind> = SecretBackendKind::ALL.iter().copied().collect();
let sources: Vec<SecretSource> = std::iter::once(SecretSource::Literal("bare".into()))
.chain(
canonical_secret_backend_kind_samples()
.into_iter()
.map(|(backend, _)| SecretSource::Backend(backend)),
)
.collect();
for source in &sources {
assert!(
declared.contains(&source.backend_kind()),
"SecretSource::backend_kind on {source:?} produced \
a kind outside SecretBackendKind::ALL",
);
}
}
#[test]
fn secret_source_backend_kind_covers_every_secret_backend_kind() {
use std::collections::HashSet;
let mut witnessed: HashSet<SecretBackendKind> = HashSet::new();
witnessed.insert(SecretSource::Literal("bare".into()).backend_kind());
for (backend, _) in canonical_secret_backend_kind_samples() {
witnessed.insert(SecretSource::Backend(backend).backend_kind());
}
let declared: HashSet<SecretBackendKind> = SecretBackendKind::ALL.iter().copied().collect();
assert_eq!(
witnessed, declared,
"SecretSource::backend_kind must cover every SecretBackendKind cell",
);
}
#[test]
fn secret_source_resolve_dispatch_partitions_by_backend_kind() {
use std::collections::HashSet;
let bare = SecretSource::Literal("bare".into());
let result = resolve(&bare);
assert!(
matches!(result.as_deref(), Ok("bare")),
"SecretSource::Literal must resolve to its bare payload",
);
assert_eq!(bare.backend_kind(), SecretBackendKind::Literal);
let mut witnessed: HashSet<SecretBackendKind> = HashSet::new();
witnessed.insert(bare.backend_kind());
for (backend, expected_kind) in canonical_secret_backend_kind_samples() {
let source = SecretSource::Backend(backend);
let r = resolve(&source);
if matches!(expected_kind, SecretBackendKind::Literal) {
assert!(
r.is_ok(),
"SecretSource::Backend(SecretBackend::Literal) must resolve to Ok",
);
}
witnessed.insert(source.backend_kind());
}
let declared: HashSet<SecretBackendKind> = SecretBackendKind::ALL.iter().copied().collect();
assert_eq!(
witnessed, declared,
"resolve dispatch over SecretSource must reach every \
SecretBackendKind cell via the backend_kind projection",
);
}
fn canonical_sops_ref_shape_samples() -> Vec<(SopsRef, SecretRefShape)> {
vec![
(
SopsRef::File(PathBuf::from("secrets/prod.yaml")),
SecretRefShape::Whole,
),
(
SopsRef::Field {
file: PathBuf::from("secrets/prod.yaml"),
field: "jwt_secret".into(),
},
SecretRefShape::Field,
),
]
}
fn canonical_vault_ref_shape_samples() -> Vec<(VaultRef, SecretRefShape)> {
vec![
(
VaultRef::Path("secret/data/prod/app".into()),
SecretRefShape::Whole,
),
(
VaultRef::Field {
path: "secret/data/prod/app".into(),
field: "password".into(),
},
SecretRefShape::Field,
),
]
}
#[test]
fn sops_ref_shape_classifies_each_variant() {
for (sops, expected) in canonical_sops_ref_shape_samples() {
assert_eq!(
sops.shape(),
expected,
"SopsRef::shape must classify {sops:?} as {expected:?}",
);
}
}
#[test]
fn vault_ref_shape_classifies_each_variant() {
for (vault, expected) in canonical_vault_ref_shape_samples() {
assert_eq!(
vault.shape(),
expected,
"VaultRef::shape must classify {vault:?} as {expected:?}",
);
}
}
#[test]
fn secret_ref_shape_is_data_free() {
for path in ["", "a.yaml", "/very/long/path/to/b.json"] {
assert_eq!(
SopsRef::File(PathBuf::from(path)).shape(),
SecretRefShape::Whole,
);
}
for (file, field) in [("", ""), ("a.yaml", "k"), ("/p/q.json", "deeply.nested.k")] {
assert_eq!(
SopsRef::Field {
file: PathBuf::from(file),
field: field.into(),
}
.shape(),
SecretRefShape::Field,
);
}
for p in ["", "p", "secret/data/prod/app"] {
assert_eq!(VaultRef::Path(p.into()).shape(), SecretRefShape::Whole);
}
for (path, field) in [("", ""), ("p", "f"), ("secret/data/x", "password")] {
assert_eq!(
VaultRef::Field {
path: path.into(),
field: field.into(),
}
.shape(),
SecretRefShape::Field,
);
}
}
#[test]
fn secret_ref_shape_is_static_and_copy_and_hashable() {
fn assert_static<T: 'static>() {}
use std::collections::HashSet;
let mut set: HashSet<SecretRefShape> = SecretRefShape::ALL.iter().copied().collect();
set.insert(SecretRefShape::Whole); assert_eq!(set.len(), SecretRefShape::ALL.len());
let s = SecretRefShape::Field;
let s2 = s;
let s3 = s;
assert_eq!(s, s2);
assert_eq!(s2, s3);
assert_static::<SecretRefShape>();
}
#[test]
fn secret_ref_shape_all_has_no_duplicates() {
use std::collections::HashSet;
let set: HashSet<SecretRefShape> = SecretRefShape::ALL.iter().copied().collect();
assert_eq!(
set.len(),
SecretRefShape::ALL.len(),
"SecretRefShape::ALL must contain no duplicates; got: {:?}",
SecretRefShape::ALL,
);
}
#[test]
fn secret_ref_shape_all_covers_both_ref_types() {
use std::collections::HashSet;
let declared: HashSet<SecretRefShape> = SecretRefShape::ALL.iter().copied().collect();
let observed: HashSet<SecretRefShape> = canonical_sops_ref_shape_samples()
.iter()
.map(|(s, _)| s.shape())
.chain(
canonical_vault_ref_shape_samples()
.iter()
.map(|(v, _)| v.shape()),
)
.collect();
assert!(
observed.is_subset(&declared),
"SopsRef::shape ∪ VaultRef::shape image must lie in \
SecretRefShape::ALL; observed: {observed:?}, declared: {declared:?}",
);
}
#[test]
fn secret_ref_shape_all_equals_union_of_ref_images() {
use std::collections::HashSet;
let declared: HashSet<SecretRefShape> = SecretRefShape::ALL.iter().copied().collect();
let observed: HashSet<SecretRefShape> = canonical_sops_ref_shape_samples()
.iter()
.map(|(s, _)| s.shape())
.chain(
canonical_vault_ref_shape_samples()
.iter()
.map(|(v, _)| v.shape()),
)
.collect();
assert_eq!(
observed, declared,
"(SopsRef ∪ VaultRef)::shape image must equal SecretRefShape::ALL",
);
}
#[test]
fn secret_ref_shape_sops_and_vault_agree_pointwise() {
assert_eq!(
SopsRef::File(PathBuf::from("a")).shape(),
VaultRef::Path("a".into()).shape(),
);
assert_eq!(
SopsRef::Field {
file: PathBuf::from("a"),
field: "k".into(),
}
.shape(),
VaultRef::Field {
path: "a".into(),
field: "k".into(),
}
.shape(),
);
}
#[test]
fn secret_ref_shape_all_declaration_order_matches_ref_variants() {
assert_eq!(
SecretRefShape::ALL,
&[SecretRefShape::Whole, SecretRefShape::Field]
);
}
#[test]
fn secret_ref_shape_as_str_yields_canonical_lowercase_names() {
assert_eq!(SecretRefShape::Whole.as_str(), "whole");
assert_eq!(SecretRefShape::Field.as_str(), "field");
}
}