pub mod prelude {
pub use super::{
get_env,
get_env_or,
get_env_or_default,
get_env_or_panic,
get_secret,
};
}
#[must_use]
fn get_env_internal(name: &str) -> Option<String> {
match std::env::var(name) {
Ok(value) => Some(value),
Err(std::env::VarError::NotPresent) => None,
Err(std::env::VarError::NotUnicode(_)) => {
tracing::error!("Failed to read environment variable `{name}`: Not Unicode.");
None
},
}
}
fn read_secret_file(path: &str) -> std::io::Result<String> {
let content = std::fs::read_to_string(path)?;
Ok(content.trim().to_string())
}
#[must_use = "Ignoring the result may cause unexpected behavior due to missing or invalid configuration."]
pub fn get_env<T: std::str::FromStr>(
name: &str,
has_secret: bool,
) -> Option<T> {
if has_secret {
let mut secret_name = name.to_owned();
secret_name.push_str("_FILE");
if let Some(value) = get_secret(&secret_name) {
return Some(value);
}
}
let value = get_env_internal(name)?;
match value.parse() {
Ok(parsed_value) => Some(parsed_value),
Err(_) => {
tracing::error!("Failed to parse environment variable `{name}`.");
None
},
}
}
#[must_use = "Secrets should be handled explicitly; ignoring the result may lead to misconfiguration."]
pub fn get_secret<T: std::str::FromStr>(
name: &str,
) -> Option<T> {
let path = get_env_internal(name)?;
match read_secret_file(&path) {
Ok(value) => {
match value.parse() {
Ok(parsed_value) => Some(parsed_value),
Err(_) => {
tracing::error!("Failed to parse Docker secret `{name}`.");
None
},
}
},
Err(error) => {
tracing::error!("Failed to read Docker secret: {error}");
None
},
}
}
#[must_use = "Ignoring the result may hide misconfigured or missing environment variables."]
pub fn get_env_or<T: std::str::FromStr>(
name: &str,
default: T,
has_secret: bool,
) -> T {
get_env(name, has_secret).unwrap_or(default)
}
#[must_use = "Ignoring the result may hide misconfigured or missing environment variables."]
pub fn get_env_or_default<T: std::str::FromStr + Default>(
name: &str,
has_secret: bool,
) -> T {
get_env(name, has_secret).unwrap_or_default()
}
#[must_use = "This function panics if the variable is missing; ignoring the result defeats its purpose."]
pub fn get_env_or_panic<T: std::str::FromStr>(
name: &str,
has_secret: bool,
) -> T {
get_env(name, has_secret).unwrap_or_else(|| {
panic!("Environment variable `{name}` is required but not set.");
})
}
#[cfg(test)]
mod tests {
use std::{
env,
io::Write,
};
use serial_test::serial;
use tempfile::NamedTempFile;
use super::*;
#[test]
#[serial]
fn test_get_env_internal() {
unsafe {
env::set_var("TEST_ENV", "test_value");
}
assert_eq!(get_env_internal("TEST_ENV"), Some("test_value".to_string()));
unsafe {
env::remove_var("TEST_ENV");
}
assert_eq!(get_env_internal("TEST_ENV"), None);
}
#[cfg(target_family = "unix")]
#[test]
#[serial]
fn test_get_env_internal_invalid_unicode_linux() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let invalid_utf8 = OsString::from_vec(vec![0xff, 0xfe, 0xfd]);
unsafe {
std::env::set_var("INVALID_UNICODE_ENV", &invalid_utf8);
}
assert_eq!(get_env_internal("INVALID_UNICODE_ENV"), None);
unsafe {
std::env::remove_var("INVALID_UNICODE_ENV");
}
}
#[test]
#[serial]
fn test_read_secret_file() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "secret_value").unwrap();
let path = temp_file.path().to_str().unwrap();
let result = read_secret_file(path).unwrap();
assert_eq!(result, "secret_value");
let invalid_path = "/invalid/path/to/secret";
assert!(read_secret_file(invalid_path).is_err());
}
#[test]
#[serial]
fn test_get_env() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "docker_secret_value").unwrap();
let path = temp_file.path().to_str().unwrap();
unsafe {
env::set_var("TEST_ENV_FILE", path); env::set_var("TEST_ENV", "env_value"); }
assert_eq!(get_env("TEST_ENV", true), Some("docker_secret_value".to_string()));
assert_eq!(get_env("TEST_ENV", false), Some("env_value".to_string()));
unsafe {
env::remove_var("TEST_ENV_FILE");
env::remove_var("TEST_ENV");
}
}
#[test]
#[serial]
fn test_get_env_or_panic() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "required_secret_value").unwrap();
let path = temp_file.path().to_str().unwrap();
unsafe {
env::set_var("REQUIRED_ENV_FILE", path);
}
assert_eq!(get_env_or_panic::<String>("REQUIRED_ENV", true), "required_secret_value".to_string());
unsafe {
env::set_var("REQUIRED_ENV", "required_env_value");
}
assert_eq!(get_env_or_panic::<String>("REQUIRED_ENV", false), "required_env_value".to_string());
unsafe {
env::remove_var("REQUIRED_ENV_FILE");
env::remove_var("REQUIRED_ENV");
}
let result = std::panic::catch_unwind(|| {
_ = get_env_or_panic::<String>("REQUIRED_ENV", true);
});
assert!(result.is_err());
}
#[test]
#[serial]
fn test_get_env_or_variants() {
unsafe {
env::remove_var("OPTIONAL_ENV");
}
assert_eq!(get_env_or("OPTIONAL_ENV", 123u32, false), 123);
assert_eq!(get_env_or_default::<u32>("OPTIONAL_ENV", false), 0);
unsafe {
env::set_var("OPTIONAL_ENV", "456");
}
assert_eq!(get_env_or("OPTIONAL_ENV", 123u32, false), 456);
assert_eq!(get_env_or_default::<u32>("OPTIONAL_ENV", false), 456);
}
}