Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for floki to interpret host environmental variables #294

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
15 changes: 15 additions & 0 deletions docs/content/intro/feature-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... there's a difference between an envvar being absent and being empty (though by default bash will expand $FOO to "" if FOO is unset - but see set -u and the various ${foo:bar}/%{foo:-bar} syntaxes).

I'm not sure what the right option is here, though I'd probably guess that erroring is more intuitive (the script expects to be using the value, if there is no value the script may well do the wrong thing if it's forced to treat it as "").

52 changes: 47 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -100,12 +102,22 @@ impl FlokiConfig {
pub fn from_file(file: &path::Path) -> Result<FlokiConfig, Error> {
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,
Expand Down Expand Up @@ -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".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to reserve ${<mode>:<value>} completely - so if we see -e ${lol:foo} we might want to throw an error rather than leaving it un-changed (this also allows adding additional things later e.g. -e KERNEL_VERSION=${exec:uname -r} without being a breaking change each time).

This also means that ${user_env:@*&} wants to be an error too - erroring when someone it looks like someone has tried but failed to use a feature is probably better than assuming they weren't aware of the feature and meant to type what they typed (might require an escape hatch for the incredibly rare case where someone wanted the un-expanded value)

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 {
Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down