diff --git a/CHANGELOG.md b/CHANGELOG.md index 1142279..42959d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Status: Available for use ## [Unreleased] ### Breaking Changes +- Allow templating using environmental variables in `floki.yaml`, with `${user_env:VAR}` replaced with value of the user's environmental variable `VAR` before deserialization. ### Added diff --git a/Cargo.toml b/Cargo.toml index 656b620..4d255d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ shlex = "1.1" sha2 = "0.10.7" anyhow = "1.0.71" thiserror = "1.0.40" +regex = "1" [dev-dependencies] tempfile = "3.6.0" diff --git a/docs/content/intro/feature-overview.md b/docs/content/intro/feature-overview.md index 4f5779d..3febcc5 100644 --- a/docs/content/intro/feature-overview.md +++ b/docs/content/intro/feature-overview.md @@ -222,3 +222,18 @@ Note that use of `docker_switches` may reduce the reproducibility and shareabili Nonetheless, it is useful to be able to add arbitrary switches in a pinch, just to be able to get something working. If there are things you can add with `docker_switches` which are reproducible and shareable, please raise a feature request, or go ahead and implement it yourself! + +# Using environmental variables in yaml + +Sometimes it is helpful for the `floki` config to include environment-specific values. For example, mounting a subdirectory of the user's home directory. `floki` supports this via templating. Specify environmental variables in `floki.yaml` as `${user_env:}`. Before interpreting the config, `floki` will substitute the user's environmental variables for those values. + +```yaml +docker_switches: + - -v ${user_env:HOME}/.vim:/home/build/.vim +``` + +Only alphanumeric and underscores (`[a-zA-Z0-9_]`) are supported in environmental variable names. Inclusion of other characters afer `user_env` will prevent the substitution from taking place. + +Warning: +- Use of host environmental variables may reduce the reproducibility and shareability of your `floki.yaml`. +- If an environmental variable is not found, it is interpreted as a blank string. \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 9aad666..d9dfd4e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,10 +2,12 @@ use crate::errors; use crate::image; use anyhow::Error; +use regex::{Captures, Regex}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fs::File; +use std::io::Read; use std::path; #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -100,12 +102,22 @@ impl FlokiConfig { pub fn from_file(file: &path::Path) -> Result { debug!("Reading configuration file: {:?}", file); - let f = File::open(file).map_err(|e| errors::FlokiError::ProblemOpeningConfigYaml { - name: file.display().to_string(), - error: e, - })?; + let mut contents = String::new(); - let mut config: FlokiConfig = serde_yaml::from_reader(f).map_err(|e| { + File::open(file) + .map_err(|e| errors::FlokiError::ProblemOpeningConfigYaml { + name: file.display().to_string(), + error: e, + })? + .read_to_string(&mut contents) + .map_err(|e| errors::FlokiError::ProblemReadingConfigYaml { + name: file.display().to_string(), + error: e, + })?; + + contents = Self::replace_user_env(&contents); + + let mut config: FlokiConfig = serde_yaml::from_str(contents.as_str()).map_err(|e| { errors::FlokiError::ProblemParsingConfigYaml { name: file.display().to_string(), error: e, @@ -139,6 +151,17 @@ impl FlokiConfig { Ok(config) } + + /// Replace all instances of ${user_env:VAR} in the yaml with the value of the local environmental variable "VAR". + fn replace_user_env(input_string: &str) -> String { + let user_env_regex = Regex::new(r"\$\{user_env:([a-zA-Z0-9_]*)\}").unwrap(); + user_env_regex + .replace_all(input_string, |caps: &Captures| { + let env_var_name = caps.get(1).unwrap().as_str(); + std::env::var(env_var_name).unwrap_or_default() + }) + .to_string() + } } fn default_shell() -> Shell { @@ -160,6 +183,25 @@ fn default_entrypoint() -> Entrypoint { #[cfg(test)] mod test { use super::*; + use image::Image; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct TestImageConfig { + image: Image, + } + + #[test] + fn test_user_env_config() { + std::env::set_var("image", "a_local_user_variable"); + let content = "image: prefix_${user_env:image}:1.1"; + + let expected = TestImageConfig { + image: Image::Name("prefix_a_local_user_variable:1.1".into()), + }; + let actual: TestImageConfig = + serde_yaml::from_str(&FlokiConfig::replace_user_env(content)).unwrap(); + assert!(actual == expected); + } #[derive(Debug, PartialEq, Serialize, Deserialize)] struct TestShellConfig { diff --git a/src/errors.rs b/src/errors.rs index 9d2e4f1..c67281d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -22,6 +22,9 @@ pub enum FlokiError { #[error("There was a problem opening the configuration file '{name}': {error:?}")] ProblemOpeningConfigYaml { name: String, error: io::Error }, + #[error("There was a problem reading the configuration file '{name}': {error:?}")] + ProblemReadingConfigYaml { name: String, error: io::Error }, + #[error("There was a problem parsing the configuration file '{name}': {error:?}")] ProblemParsingConfigYaml { name: String,