use std::collections::BTreeSet;
use crate::errors::OrionError;
pub const RESERVED_PREFIX: &str = "ORION_SECRET_";
const ORION_PREFIX: &str = "ORION_";
const SECTION_SEPARATOR: &str = "__";
const SUGGESTION_MAX_DISTANCE: usize = 3;
pub(super) fn reject_unknown_env_vars<I>(
present: I,
known: &BTreeSet<String>,
referenced_by_config_file: &BTreeSet<String>,
) -> Result<(), OrionError>
where
I: IntoIterator<Item = String>,
{
let top_level = top_level_keys(known);
let mut offenders: Vec<String> = present
.into_iter()
.filter(|name| name.starts_with(ORION_PREFIX))
.filter(|name| !name.starts_with(RESERVED_PREFIX))
.filter(|name| !known.contains(name))
.filter(|name| !referenced_by_config_file.contains(name))
.filter(|name| !super::retired_env::is_retired(name))
.filter(|name| could_be_a_misspelled_override(name, &top_level))
.map(|name| match nearest_key(&name, known) {
Some(suggestion) => format!(" {name} (did you mean {suggestion}?)"),
None => format!(" {name}"),
})
.collect();
if offenders.is_empty() {
return Ok(());
}
offenders.sort();
Err(OrionError::Config {
message: format!(
"these ORION_* environment variables are not Orion settings and would be \
silently ignored:\n{}\nEvery override is ORION_ + the config field path, \
uppercased with __ between levels — docs/src/configuration/reference.md \
lists all of them. {RESERVED_PREFIX}* is reserved for values you \
reference yourself (env:// connector secrets, ${{VAR}} placeholders in \
the config file or in a connector config_json) and is never read as \
configuration.",
offenders.join("\n")
),
})
}
pub fn looks_like_env_override(name: &str, known: &BTreeSet<String>) -> bool {
if !name.starts_with(ORION_PREFIX) || name.starts_with(RESERVED_PREFIX) {
return false;
}
could_be_a_misspelled_override(name, &top_level_keys(known))
}
fn top_level_keys(known: &BTreeSet<String>) -> Vec<&str> {
known
.iter()
.filter(|key| !key.contains(SECTION_SEPARATOR))
.map(String::as_str)
.collect()
}
fn could_be_a_misspelled_override(name: &str, top_level: &[&str]) -> bool {
name.contains(SECTION_SEPARATOR)
|| top_level
.iter()
.any(|key| edit_distance(name, key) <= SUGGESTION_MAX_DISTANCE)
}
fn nearest_key(candidate: &str, known: &BTreeSet<String>) -> Option<String> {
known
.iter()
.map(|key| (edit_distance(candidate, key), key))
.filter(|(distance, _)| *distance <= SUGGESTION_MAX_DISTANCE)
.min_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(b.1)))
.map(|(_, key)| key.clone())
}
fn edit_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
if a.is_empty() {
return b.len();
}
let mut previous: Vec<usize> = (0..=b.len()).collect();
let mut current = vec![0usize; b.len() + 1];
for (i, ca) in a.iter().enumerate() {
current[0] = i + 1;
for (j, cb) in b.iter().enumerate() {
let substitution = previous[j] + usize::from(ca != cb);
current[j + 1] = substitution.min(previous[j + 1] + 1).min(current[j] + 1);
}
std::mem::swap(&mut previous, &mut current);
}
previous[b.len()]
}
#[cfg(test)]
mod tests {
use super::super::env_overrides::known_env_override_keys;
use super::*;
fn names(list: &[&str]) -> Vec<String> {
list.iter().map(|s| (*s).to_string()).collect()
}
fn empty() -> BTreeSet<String> {
BTreeSet::new()
}
#[test]
fn a_typo_is_refused_and_names_the_nearest_key() {
let known = known_env_override_keys();
let err = reject_unknown_env_vars(names(&["ORION_SERVER__PORTT"]), &known, &empty())
.expect_err("a misspelled override must not be ignored");
let message = err.to_string();
assert!(message.contains("ORION_SERVER__PORTT"), "{message}");
assert!(
message.contains("did you mean ORION_SERVER__PORT?"),
"{message}"
);
}
#[test]
fn an_unrecognisable_name_is_refused_without_a_suggestion() {
let known = known_env_override_keys();
let err = reject_unknown_env_vars(names(&["ORION_QUANTUM__FLUX"]), &known, &empty())
.expect_err("an unknown variable must be refused");
let message = err.to_string();
assert!(message.contains("ORION_QUANTUM__FLUX"), "{message}");
assert!(!message.contains("did you mean"), "{message}");
}
#[test]
fn every_real_override_is_accepted() {
let known = known_env_override_keys();
assert!(
known.len() > 90,
"the override set looks broken: {} names",
known.len()
);
let present: Vec<String> = known.iter().cloned().collect();
reject_unknown_env_vars(present, &known, &empty())
.expect("every override Orion reads must be accepted");
}
#[test]
fn unrelated_variables_are_ignored() {
let known = known_env_override_keys();
reject_unknown_env_vars(
names(&["PATH", "HOME", "RUST_LOG", "KUBERNETES_SERVICE_HOST"]),
&known,
&empty(),
)
.expect("only ORION_* names are checked");
}
#[test]
fn a_retired_name_is_left_to_the_retired_check() {
let known = known_env_override_keys();
reject_unknown_env_vars(
names(&[
"ORION_ENV",
"ORION_QUEUE__WORKERS",
"ORION_KAFKA__MAX_INFLIGHT",
]),
&known,
&empty(),
)
.expect("retired names belong to reject_retired_env_vars");
let err = super::super::retired_env::reject_retired_env_vars(|key| {
if key == "ORION_ENV" {
Ok("production".to_string())
} else {
Err(std::env::VarError::NotPresent)
}
})
.expect_err("ORION_ENV must still report its rename");
assert!(err.to_string().contains("ORION_ENVIRONMENT"), "{err}");
}
#[test]
fn a_variable_the_config_file_references_is_allowed() {
let known = known_env_override_keys();
let referenced: BTreeSet<String> = ["ORION_DB__PASSWORD".to_string()].into_iter().collect();
reject_unknown_env_vars(names(&["ORION_DB__PASSWORD"]), &known, &referenced)
.expect("a name the config file substitutes must be allowed");
assert!(
reject_unknown_env_vars(names(&["ORION_DB__PASSWORD"]), &known, &empty()).is_err(),
"without the reference it is just an unknown variable"
);
}
#[test]
fn the_reserved_prefix_is_never_interpreted() {
let known = known_env_override_keys();
reject_unknown_env_vars(
names(&["ORION_SECRET_DB_PASSWORD", "ORION_SECRET_SERVER__PORTT"]),
&known,
&empty(),
)
.expect("the reserved namespace is never read as configuration");
}
fn injected_service_links() -> Vec<String> {
names(&[
"ORION_SERVICE_HOST",
"ORION_SERVICE_PORT",
"ORION_SERVICE_PORT_HTTP",
"ORION_PORT",
"ORION_PORT_8080_TCP",
"ORION_PORT_8080_TCP_PROTO",
"ORION_PORT_8080_TCP_PORT",
"ORION_PORT_8080_TCP_ADDR",
"ORION_POSTGRES_SERVICE_HOST",
"ORION_POSTGRES_SERVICE_PORT",
"ORION_POSTGRES_PORT",
"ORION_POSTGRES_PORT_5432_TCP",
"ORION_POSTGRES_PORT_5432_TCP_PROTO",
"ORION_POSTGRES_PORT_5432_TCP_PORT",
"ORION_POSTGRES_PORT_5432_TCP_ADDR",
"ORION_REDIS_SERVICE_HOST",
"ORION_REDIS_SERVICE_PORT",
"ORION_REDIS_PORT",
"ORION_REDIS_PORT_6379_TCP",
"ORION_REDIS_PORT_6379_TCP_PROTO",
"ORION_REDIS_PORT_6379_TCP_PORT",
"ORION_REDIS_PORT_6379_TCP_ADDR",
])
}
#[test]
fn separatorless_names_are_not_configuration() {
let known = known_env_override_keys();
let mut present = injected_service_links();
present.extend(names(&[
"ORION_SERVER_URL",
"ORION_API_KEY",
"ORION_URL",
"ORION_VERSION",
"ORION_IMAGE",
"ORION_BIN",
"ORION_SERVER_PORT",
]));
reject_unknown_env_vars(present, &known, &empty())
.expect("a name without the section separator is not an override, misspelled or not");
}
#[test]
fn a_typo_survives_alongside_the_injected_block() {
let known = known_env_override_keys();
let mut present = injected_service_links();
present.push("ORION_SERVER__PORTT".to_string());
let err = reject_unknown_env_vars(present, &known, &empty())
.expect_err("the typo must still be caught");
let message = err.to_string();
assert!(message.contains("ORION_SERVER__PORTT"), "{message}");
assert!(!message.contains("ORION_SERVICE_HOST"), "{message}");
assert!(!message.contains("ORION_PORT_8080"), "{message}");
}
#[test]
fn only_top_level_settings_have_no_separator() {
let separatorless: BTreeSet<String> = known_env_override_keys()
.into_iter()
.filter(|key| !key.contains(SECTION_SEPARATOR))
.collect();
assert_eq!(
separatorless,
["ORION_ENVIRONMENT".to_string()].into_iter().collect(),
"a new top-level setting joined the near-miss check — confirm no \
service link or sibling-tool variable is within \
{SUGGESTION_MAX_DISTANCE} edits of it"
);
}
#[test]
fn a_misspelled_top_level_setting_is_still_refused() {
let known = known_env_override_keys();
for typo in [
"ORION_ENVIRONMEN",
"ORION_ENVIRONMENTT",
"ORION_ENVIRONMNET",
] {
let err = reject_unknown_env_vars(names(&[typo]), &known, &empty())
.expect_err("a near-miss of a top-level setting must be refused");
let message = err.to_string();
assert!(message.contains(typo), "{message}");
assert!(
message.contains("did you mean ORION_ENVIRONMENT?"),
"{message}"
);
}
}
#[test]
fn every_offender_is_reported_in_one_pass() {
let known = known_env_override_keys();
let err = reject_unknown_env_vars(
names(&[
"ORION_SERVER__PORTT",
"ORION_METRIC__ENABLED",
"ORION_NOPE__NOPE",
]),
&known,
&empty(),
)
.expect_err("all three are unknown");
let message = err.to_string();
for expected in [
"ORION_SERVER__PORTT",
"ORION_METRIC__ENABLED",
"ORION_NOPE__NOPE",
] {
assert!(message.contains(expected), "missing {expected}: {message}");
}
}
#[test]
fn edit_distance_counts_single_character_edits() {
assert_eq!(edit_distance("", ""), 0);
assert_eq!(edit_distance("", "abc"), 3);
assert_eq!(edit_distance("abc", ""), 3);
assert_eq!(edit_distance("abc", "abc"), 0);
assert_eq!(edit_distance("abc", "abd"), 1);
assert_eq!(edit_distance("abc", "abcd"), 1);
assert_eq!(edit_distance("abc", "ac"), 1);
}
}