use core::convert::Infallible;
use core::fmt;
use crate::secrets::{Resolution, SecretRef, SecretView};
const TOKENS: &[&str] = &["instance", "name"];
const SECRET_PREFIX: &str = "secret:";
pub(crate) fn secret_reference(token: &str) -> Option<SecretRef<'_>> {
token.strip_prefix(SECRET_PREFIX).and_then(SecretRef::parse)
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TemplateError {
UnknownToken {
token: String,
},
Unclosed,
}
impl fmt::Display for TemplateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownToken { token } if token.starts_with(SECRET_PREFIX) => write!(
f,
"`{{{{{token}}}}}` is not a valid secret reference: write \
`{{{{secret:KEY}}}}` or `{{{{secret:namespace/KEY}}}}`, where each part \
holds only letters, digits, `.`, `_` or `-` and does not start with `.`"
),
Self::UnknownToken { token } => write!(
f,
"`{{{{{token}}}}}` is not a template token: valid tokens are {}",
TOKENS
.iter()
.map(|t| format!("`{{{{{t}}}}}`"))
.chain(core::iter::once(format!("`{{{{{SECRET_PREFIX}...}}}}`")))
.collect::<Vec<_>>()
.join(", ")
),
Self::Unclosed => f.write_str("a `{{` in this value is never closed by a `}}`"),
}
}
}
impl core::error::Error for TemplateError {}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderError {
Unresolved {
reference: String,
environment: String,
},
NamespaceUnready {
namespace: String,
reference: String,
environment: String,
},
}
impl RenderError {
#[must_use]
pub fn is_retriable(&self) -> bool {
matches!(self, Self::NamespaceUnready { .. })
}
}
impl fmt::Display for RenderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unresolved {
reference,
environment,
} => write!(
f,
"`{reference}` has no value in the `{environment}` environment"
),
Self::NamespaceUnready {
namespace,
reference,
environment,
} => write!(
f,
"`{reference}` reads the `{namespace}` namespace, which no provider dog \
has pushed to for the `{environment}` environment yet"
),
}
}
}
impl core::error::Error for RenderError {}
pub(crate) enum Segment<'a> {
Literal(&'a str),
Token(&'a str),
}
pub(crate) enum Completion {
Complete,
Unclosed,
}
pub(crate) fn walk<E>(
value: &str,
mut on_segment: impl FnMut(Segment<'_>) -> Result<(), E>,
) -> Result<Completion, E> {
let bytes = value.as_bytes();
let mut at = 0;
let mut literal_from = 0;
while at < bytes.len() {
if bytes[at..].starts_with(b"{{{{") {
on_segment(Segment::Literal(&value[literal_from..at]))?;
on_segment(Segment::Literal("{{"))?;
at += 4;
literal_from = at;
} else if bytes[at..].starts_with(b"}}}}") {
on_segment(Segment::Literal(&value[literal_from..at]))?;
on_segment(Segment::Literal("}}"))?;
at += 4;
literal_from = at;
} else if bytes[at..].starts_with(b"{{") {
on_segment(Segment::Literal(&value[literal_from..at]))?;
let rest = &value[at + 2..];
let Some(end) = rest.find("}}") else {
return Ok(Completion::Unclosed);
};
on_segment(Segment::Token(&rest[..end]))?;
at += 2 + end + 2;
literal_from = at;
} else {
at += 1;
}
}
on_segment(Segment::Literal(&value[literal_from..]))?;
Ok(Completion::Complete)
}
fn push_token(out: &mut String, token: &str) {
out.push_str("{{");
out.push_str(token);
out.push_str("}}");
}
fn resolve_secret<'a>(
reference: &SecretRef<'_>,
secrets: &'a SecretView,
) -> Result<&'a str, RenderError> {
match (secrets.resolve(reference), reference.namespace) {
(Resolution::Found(value), _) => Ok(value),
(Resolution::MissingNamespace, Some(namespace)) => Err(RenderError::NamespaceUnready {
namespace: namespace.to_string(),
reference: reference.to_string(),
environment: secrets.environment().to_string(),
}),
(Resolution::MissingKey | Resolution::MissingNamespace, _) => {
Err(RenderError::Unresolved {
reference: reference.to_string(),
environment: secrets.environment().to_string(),
})
}
}
}
pub(crate) fn holds_secret(value: &str) -> bool {
let mut found = false;
let _ = walk::<Infallible>(value, |segment| {
if let Segment::Token(token) = segment
&& secret_reference(token).is_some()
{
found = true;
}
Ok(())
});
found
}
pub(crate) fn validate(value: &str) -> Result<(), TemplateError> {
let completion = walk(value, |segment| match segment {
Segment::Literal(_) => Ok(()),
Segment::Token(token) if TOKENS.contains(&token) || secret_reference(token).is_some() => {
Ok(())
}
Segment::Token(token) => Err(TemplateError::UnknownToken {
token: token.to_string(),
}),
})?;
match completion {
Completion::Complete => Ok(()),
Completion::Unclosed => Err(TemplateError::Unclosed),
}
}
#[must_use]
pub fn render_positional(value: &str, name: &str, instance: u32) -> String {
let mut out = String::with_capacity(value.len());
let slot = instance.to_string();
let _: Result<Completion, Infallible> = walk(value, |segment| {
match segment {
Segment::Literal(literal) => out.push_str(literal),
Segment::Token("instance") => out.push_str(&slot),
Segment::Token("name") => out.push_str(name),
Segment::Token(token) => push_token(&mut out, token),
}
Ok(())
});
out
}
pub fn render(
value: &str,
name: &str,
instance: u32,
secrets: &SecretView,
) -> Result<String, RenderError> {
let mut out = String::with_capacity(value.len());
let slot = instance.to_string();
walk(value, |segment| {
match segment {
Segment::Literal(literal) => out.push_str(literal),
Segment::Token("instance") => out.push_str(&slot),
Segment::Token("name") => out.push_str(name),
Segment::Token(token) => match secret_reference(token) {
Some(reference) => out.push_str(resolve_secret(&reference, secrets)?),
None => push_token(&mut out, token),
},
}
Ok(())
})?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_two_tokens_render() {
assert_eq!(render_positional("z-{{instance}}", "worker", 3), "z-3");
assert_eq!(
render_positional("{{name}}-{{instance}}d", "worker", 3),
"worker-3d"
);
assert_eq!(render_positional("91{{instance}}", "worker", 7), "917");
}
#[test]
fn a_value_with_no_token_is_returned_unchanged() {
let empty = SecretView::empty("production".to_string());
for value in [
r#"{"ts":"%t","level":"%l"}"#,
r#"{"a":{"b":1}}"#,
"^[a-z]{2,3}$",
"plain",
] {
assert_eq!(
render_positional(value, "worker", 1),
value,
"unchanged: {value}"
);
assert_eq!(
render(value, "worker", 1, &empty).unwrap(),
value,
"unchanged: {value}"
);
assert!(validate(value).is_ok(), "and accepted: {value}");
}
}
#[test]
fn an_unknown_token_is_refused_by_name() {
let err = validate("z-{{instnace}}").unwrap_err();
assert!(matches!(&err, TemplateError::UnknownToken { token } if token == "instnace"));
let rendered = err.to_string();
assert!(rendered.contains("instnace"), "names the typo: {rendered}");
assert!(
rendered.contains("instance"),
"and what is valid: {rendered}"
);
assert!(
!rendered.contains('\u{2014}') && !rendered.contains('\u{2013}'),
"no em or en dash in copy a user reads: {rendered}"
);
}
#[test]
fn doubling_escapes_a_literal_token() {
assert_eq!(
render_positional("{{{{instance}}}}", "worker", 3),
"{{instance}}"
);
assert!(validate("{{{{ .Values.port }}}}").is_ok());
assert_eq!(
render_positional("{{{{ .Values.port }}}}", "worker", 3),
"{{ .Values.port }}",
"a Helm template passes through for the tool that consumes it"
);
}
#[test]
fn an_unclosed_token_is_refused() {
assert!(validate("z-{{instance").is_err());
}
fn view(environment: &str) -> SecretView {
use crate::secrets::ProviderCache;
use std::collections::{BTreeMap, BTreeSet};
let store = BTreeMap::from([(
"DB_PASSWORD".to_string(),
BTreeMap::from([("production".to_string(), "hunter2".to_string())]),
)]);
let providers = ProviderCache {
values: BTreeMap::from([(
"vercel".to_string(),
BTreeMap::from([(
"API_KEY".to_string(),
BTreeMap::from([("production".to_string(), "sk_live".to_string())]),
)]),
)]),
pushed: BTreeMap::from([(
"vercel".to_string(),
BTreeSet::from(["production".to_string()]),
)]),
};
SecretView::new(environment.to_string(), store, providers)
}
#[test]
fn a_secret_token_validates_with_and_without_a_namespace() {
assert!(validate("{{secret:DB_PASSWORD}}").is_ok());
assert!(validate("{{secret:vercel/API_KEY}}").is_ok());
assert!(validate("postgres://u:{{secret:DB_PASSWORD}}@db/app").is_ok());
}
#[test]
fn a_malformed_reference_is_refused_at_config_time() {
for bad in [
"{{secret:}}",
"{{secret:/KEY}}",
"{{secret:ns/}}",
"{{secret:a/b/c}}",
"{{secret:has space}}",
] {
let err = validate(bad).unwrap_err();
let rendered = err.to_string();
assert!(rendered.contains("secret"), "{bad}: {rendered}");
}
}
#[test]
fn an_unknown_prefix_is_still_refused_by_name() {
let err = validate("{{sekret:K}}").unwrap_err();
assert!(matches!(&err, TemplateError::UnknownToken { token } if token == "sekret:K"));
}
#[test]
fn render_substitutes_a_resolved_secret() {
assert_eq!(
render("pw={{secret:DB_PASSWORD}}", "web", 0, &view("production")).unwrap(),
"pw=hunter2"
);
assert_eq!(
render("{{secret:vercel/API_KEY}}", "web", 0, &view("production")).unwrap(),
"sk_live"
);
}
#[test]
fn positional_tokens_still_render_beside_a_secret() {
assert_eq!(
render(
"{{name}}-{{instance}}-{{secret:DB_PASSWORD}}",
"web",
3,
&view("production")
)
.unwrap(),
"web-3-hunter2"
);
}
#[test]
fn an_unresolvable_key_errors_naming_the_reference_and_the_environment() {
let err = render("{{secret:ABSENT}}", "web", 0, &view("production")).unwrap_err();
assert!(!err.is_retriable(), "a missing key is nobody's to retry");
let rendered = err.to_string();
assert!(rendered.contains("{{secret:ABSENT}}"), "{rendered}");
assert!(rendered.contains("production"), "{rendered}");
}
#[test]
fn a_secret_missing_only_in_this_environment_errors_rather_than_borrowing_another() {
let err = render("{{secret:DB_PASSWORD}}", "web", 0, &view("staging")).unwrap_err();
assert!(err.to_string().contains("staging"));
}
#[test]
fn an_unready_namespace_is_retriable_and_says_which_one() {
let err = render("{{secret:vault/ANY}}", "web", 0, &view("production")).unwrap_err();
assert!(err.is_retriable(), "no dog has pushed under this name yet");
let rendered = err.to_string();
assert!(rendered.contains("vault"), "{rendered}");
}
#[test]
fn a_namespace_that_is_up_and_lacks_the_key_is_not_retriable() {
let err = render("{{secret:vercel/ABSENT}}", "web", 0, &view("production")).unwrap_err();
assert!(!err.is_retriable());
}
#[test]
fn no_render_error_ever_prints_a_value() {
let unresolved = render("{{secret:ABSENT}}", "web", 0, &view("production")).unwrap_err();
assert_eq!(
unresolved.to_string(),
"`{{secret:ABSENT}}` has no value in the `production` environment"
);
assert_eq!(
format!("{unresolved:?}"),
"Unresolved { reference: \"{{secret:ABSENT}}\", environment: \"production\" }"
);
let unready = render("{{secret:vault/ANY}}", "web", 0, &view("production")).unwrap_err();
assert_eq!(
unready.to_string(),
"`{{secret:vault/ANY}}` reads the `vault` namespace, which no provider dog \
has pushed to for the `production` environment yet"
);
assert_eq!(
format!("{unready:?}"),
"NamespaceUnready { namespace: \"vault\", reference: \"{{secret:vault/ANY}}\", \
environment: \"production\" }"
);
for rendered in [unresolved.to_string(), unready.to_string()] {
assert!(
!rendered.contains('\u{2014}') && !rendered.contains('\u{2013}'),
"no em or en dash in copy a user reads: {rendered}"
);
}
}
#[test]
fn render_positional_leaves_a_secret_token_alone() {
assert_eq!(
render_positional("{{secret:DB_PASSWORD}}-{{instance}}", "web", 2),
"{{secret:DB_PASSWORD}}-2"
);
}
#[test]
fn doubling_still_escapes_a_secret_token() {
assert_eq!(
render("{{{{secret:DB_PASSWORD}}}}", "web", 0, &view("production")).unwrap(),
"{{secret:DB_PASSWORD}}"
);
}
}