docker_env/
lib.rs

1//! # docker-env
2//! `docker-env` is a minimal utility crate for reading environment variables
3//! and Docker-style secrets in containerised Rust applications.
4//!
5//! This crate is designed for use in Docker, Kubernetes, and other container
6//! environments where secrets may be injected as file-based mounts rather than
7//! plain environment variables.
8//!
9//! ## Features
10//! - Typed access to environment variables (`get_env<T>`, `get_env_or`, etc.).
11//! - Support for secret files via the `_FILE` suffix convention.
12//! - Graceful fallbacks with optional, default, or required values.
13//! - Logs errors using the `tracing` crate.
14//!
15//! ## Docker Secret Convention
16//! When a variable has a secret counterpart (e.g., `DATABASE_PASSWORD`),
17//! Docker often mounts the secret as a file and sets an accompanying
18//! environment variable with the `_FILE` suffix:
19//!
20//! ```bash
21//! DATABASE_PASSWORD_FILE=/run/secrets/db_password
22//! ```
23//!
24//! Calling:
25//!
26//! ```no_run
27//! let password: String = docker_env::get_env("DATABASE_PASSWORD", true).unwrap();
28//! ```
29//!
30//! will cause `docker-env` to read the contents of the file at
31//! `/run/secrets/db_password` instead of the plain `DATABASE_PASSWORD`
32//! variable.
33//!
34//! ## Example
35//!
36//! ```no_run
37//! use docker_env::{get_env, get_env_or, get_env_or_panic};
38//!
39//! let db_url: String = get_env_or_panic("DATABASE_URL", false);
40//! let port: u16 = get_env_or("PORT", 8080, false);
41//! let api_key: String = get_env("API_KEY", true).unwrap();
42//! ```
43//!
44//! ## When to Use
45//!
46//! Use this crate when your app:
47//! - Runs in a Docker or Kubernetes environment
48//! - Uses file-based secrets via `/run/secrets/` or similar
49//! - Requires simple, typed configuration with error logging
50
51/// Commonly used imports for convenience.
52pub mod prelude {
53    pub use super::{
54        get_env,
55        get_env_or,
56        get_env_or_default,
57        get_env_or_panic,
58        get_secret,
59    };
60}
61
62/// Reads and returns the value of an environment variable.
63#[must_use]
64fn get_env_internal(name: &str) -> Option<String> {
65    match std::env::var(name) {
66        Ok(value) => Some(value),
67        Err(std::env::VarError::NotPresent) => None,
68        Err(std::env::VarError::NotUnicode(_)) => {
69            tracing::error!("Failed to read environment variable `{name}`: Not Unicode.");
70            None
71        },
72    }
73}
74
75/// Reads the value of a Docker secret value and sanitises the output.
76/// 
77/// # Notes
78/// Typically, Docker secrets should only contain a single line; however, this
79/// function will read and return the trimmed contents of the entire file.
80fn read_secret_file(path: &str) -> std::io::Result<String> {
81    let content = std::fs::read_to_string(path)?;
82    Ok(content.trim().to_string())
83}
84
85/// Reads an environment variable.
86/// 
87/// # Docker Secrets
88/// If `has_secret` is `true`, it indicates that this environment variable has a
89/// Docker secret counterpart which should be prioritised over the original
90/// environment variable.
91/// 
92/// The name of secret environment variables is the same as the original name
93/// but with the `"_FILE"` suffix. This should point to the file that contains
94/// the secret value.
95#[must_use = "Ignoring the result may cause unexpected behavior due to missing or invalid configuration."]
96pub fn get_env<T: std::str::FromStr>(
97    name: &str,
98    has_secret: bool,
99) -> Option<T> {
100    if has_secret {
101        // Calculate the name of the secret variable.
102        let mut secret_name = name.to_owned();
103        secret_name.push_str("_FILE");
104
105        if let Some(value) = get_secret(&secret_name) {
106            return Some(value);
107        }
108    }
109
110    let value = get_env_internal(name)?;
111    match value.parse() {
112        Ok(parsed_value) => Some(parsed_value),
113        Err(_) => {
114            tracing::error!("Failed to parse environment variable `{name}`.");
115            None
116        },
117    }
118}
119
120/// Reads an environment variable containing the path to a Docker secret file.
121#[must_use = "Secrets should be handled explicitly; ignoring the result may lead to misconfiguration."]
122pub fn get_secret<T: std::str::FromStr>(
123    name: &str,
124) -> Option<T> {
125    let path = get_env_internal(name)?;
126    match read_secret_file(&path) {
127        Ok(value) => {
128            match value.parse() {
129                Ok(parsed_value) => Some(parsed_value),
130                Err(_) => {
131                    tracing::error!("Failed to parse Docker secret `{name}`.");
132                    None
133                },
134            }
135        },
136        Err(error) => {
137            tracing::error!("Failed to read Docker secret: {error}");
138            None
139        },
140    }
141}
142
143/// Reads an environment variable and returns its value if it exists; otherwise,
144/// the `default` value is returned.
145/// 
146/// For more information, see [`get_env`].
147#[must_use = "Ignoring the result may hide misconfigured or missing environment variables."]
148pub fn get_env_or<T: std::str::FromStr>(
149    name: &str,
150    default: T,
151    has_secret: bool,
152) -> T {
153    get_env(name, has_secret).unwrap_or(default)
154}
155
156/// Reads an environment variable and returns its value if it exists; otherwise,
157/// the default value is returned.
158/// 
159/// For more information, see [`get_env`].
160#[must_use = "Ignoring the result may hide misconfigured or missing environment variables."]
161pub fn get_env_or_default<T: std::str::FromStr + Default>(
162    name: &str,
163    has_secret: bool,
164) -> T {
165    get_env(name, has_secret).unwrap_or_default()
166}
167
168/// Reads an environment variable and returns its value if it exists; otherwise,
169/// this function will panic.
170/// 
171/// For more information, see [`get_env`].
172/// 
173/// # Panics
174/// This function will panic if the environment variable does not exist.
175#[must_use = "This function panics if the variable is missing; ignoring the result defeats its purpose."]
176pub fn get_env_or_panic<T: std::str::FromStr>(
177    name: &str,
178    has_secret: bool,
179) -> T {
180    get_env(name, has_secret).unwrap_or_else(|| {
181        panic!("Environment variable `{name}` is required but not set.");
182    })
183}
184
185#[cfg(test)]
186mod tests {
187    use std::{
188        env,
189        io::Write,
190    };
191    use serial_test::serial;
192    use tempfile::NamedTempFile;
193    use super::*;
194
195    #[test]
196    #[serial]
197    fn test_get_env_internal() {
198        // SAFETY: Unit tests are run on the main thread.
199        unsafe {
200            env::set_var("TEST_ENV", "test_value");
201        }
202        assert_eq!(get_env_internal("TEST_ENV"), Some("test_value".to_string()));
203
204        // SAFETY: Unit tests are run on the main thread.
205        unsafe {
206            env::remove_var("TEST_ENV");
207        }
208        assert_eq!(get_env_internal("TEST_ENV"), None);
209    }
210
211    #[cfg(target_family = "unix")]
212    #[test]
213    #[serial]
214    fn test_get_env_internal_invalid_unicode_linux() {
215        use std::ffi::OsString;
216        use std::os::unix::ffi::OsStringExt;
217
218        // Create a non-UTF8 environment variable value
219        let invalid_utf8 = OsString::from_vec(vec![0xff, 0xfe, 0xfd]);
220        // SAFETY: Unit tests are run on the main thread.
221        unsafe {
222            std::env::set_var("INVALID_UNICODE_ENV", &invalid_utf8);
223        }
224
225        // Should log an error and return None
226        assert_eq!(get_env_internal("INVALID_UNICODE_ENV"), None);
227
228        // Clean up:
229        // SAFETY: Unit tests are run on the main thread.
230        unsafe {
231            std::env::remove_var("INVALID_UNICODE_ENV");
232        }
233    }
234
235    #[test]
236    #[serial]
237    fn test_read_secret_file() {
238        let mut temp_file = NamedTempFile::new().unwrap();
239        writeln!(temp_file, "secret_value").unwrap();
240
241        let path = temp_file.path().to_str().unwrap();
242        let result = read_secret_file(path).unwrap();
243
244        assert_eq!(result, "secret_value");
245
246        // Test with a non-existent file
247        let invalid_path = "/invalid/path/to/secret";
248        assert!(read_secret_file(invalid_path).is_err());
249    }
250
251    #[test]
252    #[serial]
253    fn test_get_env() {
254        // Create Docker secret:
255        let mut temp_file = NamedTempFile::new().unwrap();
256        writeln!(temp_file, "docker_secret_value").unwrap();
257
258        // Set environment variables:
259        let path = temp_file.path().to_str().unwrap();
260        // SAFETY: Unit tests are run on the main thread.
261        unsafe {
262            env::set_var("TEST_ENV_FILE", path); // Secret path
263            env::set_var("TEST_ENV", "env_value"); // Non-secret value
264        }
265
266        // When has_secret is true, Docker secret is prioritised:
267        assert_eq!(get_env("TEST_ENV", true), Some("docker_secret_value".to_string()));
268
269        // When has_secret is false, environment variable is used:
270        assert_eq!(get_env("TEST_ENV", false), Some("env_value".to_string()));
271
272        // Clean up:
273        // SAFETY: Unit tests are run on the main thread.
274        unsafe {
275            env::remove_var("TEST_ENV_FILE");
276            env::remove_var("TEST_ENV");
277        }
278    }
279
280    #[test]
281    #[serial]
282    fn test_get_env_or_panic() {
283        let mut temp_file = NamedTempFile::new().unwrap();
284        writeln!(temp_file, "required_secret_value").unwrap();
285
286        let path = temp_file.path().to_str().unwrap();
287        // SAFETY: Unit tests are run on the main thread.
288        unsafe {
289            env::set_var("REQUIRED_ENV_FILE", path);
290        }
291
292        // When has_secret is true, Docker secret is prioritised:
293        assert_eq!(get_env_or_panic::<String>("REQUIRED_ENV", true), "required_secret_value".to_string());
294
295        // SAFETY: Unit tests are run on the main thread.
296        unsafe {
297            env::set_var("REQUIRED_ENV", "required_env_value");
298        }
299
300        // When has_secret is false, environment variable is used:
301        assert_eq!(get_env_or_panic::<String>("REQUIRED_ENV", false), "required_env_value".to_string());
302
303        // Test for missing variable:
304        // SAFETY: Unit tests are run on the main thread.
305        unsafe {
306            env::remove_var("REQUIRED_ENV_FILE");
307            env::remove_var("REQUIRED_ENV");
308        }
309
310        let result = std::panic::catch_unwind(|| {
311            _ = get_env_or_panic::<String>("REQUIRED_ENV", true);
312        });
313        assert!(result.is_err());
314    }
315
316    #[test]
317    #[serial]
318    fn test_get_env_or_variants() {
319        // SAFETY: Unit tests are run on the main thread.
320        unsafe {
321            env::remove_var("OPTIONAL_ENV");
322        }
323
324        assert_eq!(get_env_or("OPTIONAL_ENV", 123u32, false), 123);
325        assert_eq!(get_env_or_default::<u32>("OPTIONAL_ENV", false), 0);
326
327        // SAFETY:
328        unsafe {
329            env::set_var("OPTIONAL_ENV", "456");
330        }
331
332        assert_eq!(get_env_or("OPTIONAL_ENV", 123u32, false), 456);
333        assert_eq!(get_env_or_default::<u32>("OPTIONAL_ENV", false), 456);
334    }
335}