diff --git a/CHANGELOG.md b/CHANGELOG.md index 1142279..c957702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Status: Available for use ## [Unreleased] ### Breaking Changes +- Interpret floki.yaml as a template using tera. Among other function, this allows use of environmental variables in floki config. ### Added diff --git a/Cargo.toml b/Cargo.toml index 656b620..0ba0c28 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" +tera = "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..98bfa53 100644 --- a/docs/content/intro/feature-overview.md +++ b/docs/content/intro/feature-overview.md @@ -222,3 +222,12 @@ 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! + +# Templating + +`floki` supports templating using the [Tera engine](https://tera.netlify.app/). Currently this is executed as a one-shot rendering of your `floki.yaml`. Example uses include referencing environmental variables in the `floki` config, e.g. to mount a subdirectory of the user's home directory: +```yaml +docker_switches: + - -v {{ get_env(name='HOME') }}/.vim:/home/build/.vim +``` +Note that extensive use may reduce the reproducibility and shareability of your `floki.yaml` \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 9aad666..58a7330 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,9 +3,10 @@ use crate::errors; use crate::image; use anyhow::Error; use serde::{Deserialize, Serialize}; +use tera::{Context, Tera}; use std::collections::BTreeMap; -use std::fs::File; +use std::fs::read_to_string; use std::path; #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -98,14 +99,28 @@ pub(crate) struct FlokiConfig { impl FlokiConfig { pub fn from_file(file: &path::Path) -> Result { - debug!("Reading configuration file: {:?}", file); + debug!("Reading configuration file: {}", file.display()); - let f = File::open(file).map_err(|e| errors::FlokiError::ProblemOpeningConfigYaml { - name: file.display().to_string(), - error: e, + let contents = + read_to_string(file).map_err(|e| errors::FlokiError::ProblemReadingConfigFile { + name: file.display().to_string(), + error: e, + })?; + + let contents = Tera::one_off(&contents, &Context::new(), false).map_err(|e| { + errors::FlokiError::ProblemRenderingConfig { + name: file.display().to_string(), + error: e, + } })?; - let mut config: FlokiConfig = serde_yaml::from_reader(f).map_err(|e| { + debug!( + "Rendered '{}' into text configuration:\n{}", + file.display(), + contents + ); + + let mut config: FlokiConfig = serde_yaml::from_str(&contents).map_err(|e| { errors::FlokiError::ProblemParsingConfigYaml { name: file.display().to_string(), error: e, @@ -160,6 +175,30 @@ 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 expected = TestImageConfig { + image: Image::Name("prefix_a_local_user_variable:1.1".into()), + }; + + let content = Tera::one_off( + "image: \"prefix_{{ get_env(name='image') }}:1.1\"", + &Context::new(), + false, + ) + .unwrap(); + let actual: TestImageConfig = serde_yaml::from_str(&content).unwrap(); + assert!(actual == expected); + } #[derive(Debug, PartialEq, Serialize, Deserialize)] struct TestShellConfig { diff --git a/src/errors.rs b/src/errors.rs index 9d2e4f1..7fb529f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -19,8 +19,13 @@ pub enum FlokiError { #[error("Could not normalize the file path '{name}': {error:?}")] ProblemNormalizingFilePath { name: String, error: io::Error }, - #[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:?}")] + ProblemReadingConfigFile { name: String, error: io::Error }, + + #[error( + "There was a problem rendering configuration from the template file '{name}': {error:?}" + )] + ProblemRenderingConfig { name: String, error: tera::Error }, #[error("There was a problem parsing the configuration file '{name}': {error:?}")] ProblemParsingConfigYaml {