Skip to content

Commit 9f04ec5

Browse files
author
Max Dymond
authored
Merge pull request #298 from Metaswitch/md/loady
Add `yaml`,`json` and `toml` loading capability to the tera engine
2 parents 893f46f + 0e04720 commit 9f04ec5

9 files changed

Lines changed: 140 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ Status: Available for use
1414
### Breaking Changes
1515

1616
### Added
17+
- Add `yaml` function to tera templating engine so that floki templates
18+
can use `yaml(file="<filepath>")` in order to load values.
19+
- Add `json` function to tera templating engine so that floki templates
20+
can use `json(file="<filepath>")` in order to load values.
21+
- Add `toml` function to tera templating engine so that floki templates
22+
can use `toml(file="<filepath>")` in order to load values.
23+
- Add `floki render` to print out the rendered configuration template.
1724

1825
### Fixed
1926

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ sha2 = "0.10.7"
2929
anyhow = "1.0.71"
3030
thiserror = "1.0.40"
3131
tera = "1"
32+
serde_json = "1.0.100"
33+
toml = "0.5.11"
3234

3335
[dev-dependencies]
3436
tempfile = "3.6.0"

src/cli.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ pub(crate) enum Subcommand {
2020
#[structopt(name = "SHELL", parse(try_from_str))]
2121
shell: structopt::clap::Shell,
2222
},
23+
24+
/// Render the configuration file to stdout, performing any templating
25+
/// operations.
26+
#[structopt(name = "render")]
27+
Render {},
2328
}
2429

2530
/// Main CLI interface

src/config.rs

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ use crate::errors;
33
use crate::image;
44
use anyhow::Error;
55
use serde::{Deserialize, Serialize};
6+
use tera::from_value;
67
use tera::Context;
78
use tera::Tera;
89

910
use std::collections::BTreeMap;
1011
use std::collections::HashMap;
11-
use std::path;
12+
use std::path::{Path, PathBuf};
1213

1314
#[derive(Debug, PartialEq, Serialize, Deserialize)]
1415
#[serde(untagged)]
@@ -56,7 +57,7 @@ pub(crate) struct Volume {
5657
pub(crate) shared: bool,
5758
/// The mount path is the path at which the volume is mounted
5859
/// inside the floki container.
59-
pub(crate) mount: path::PathBuf,
60+
pub(crate) mount: PathBuf,
6061
}
6162

6263
#[derive(Debug, PartialEq, Serialize, Deserialize)]
@@ -83,7 +84,7 @@ pub(crate) struct FlokiConfig {
8384
#[serde(default = "default_shell")]
8485
pub(crate) shell: Shell,
8586
#[serde(default = "default_mount")]
86-
pub(crate) mount: path::PathBuf,
87+
pub(crate) mount: PathBuf,
8788
#[serde(default = "Vec::new")]
8889
pub(crate) docker_switches: Vec<String>,
8990
#[serde(default = "default_to_false")]
@@ -98,27 +99,80 @@ pub(crate) struct FlokiConfig {
9899
pub(crate) entrypoint: Entrypoint,
99100
}
100101

101-
impl FlokiConfig {
102-
pub fn from_file(file: &path::Path) -> Result<FlokiConfig, Error> {
103-
debug!("Reading configuration file: {:?}", file);
102+
fn path_from_args(args: &HashMap<String, tera::Value>) -> tera::Result<String> {
103+
let file = match args.get("file") {
104+
Some(file) => file,
105+
None => return Err("file parameter is required".into()),
106+
};
107+
Ok(from_value::<String>(file.clone())?)
108+
}
109+
110+
fn yamlloader(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
111+
let path = path_from_args(args)?;
112+
let f = std::fs::File::open(path)?;
113+
serde_yaml::from_reader(f).map_err(|_| "Failed to read file".into())
114+
}
115+
116+
fn jsonloader(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
117+
let path = path_from_args(args)?;
118+
let f = std::fs::File::open(path)?;
119+
serde_json::from_reader(f).map_err(|_| "Failed to read file".into())
120+
}
121+
122+
fn tomlloader(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
123+
let path = path_from_args(args)?;
124+
let contents = std::fs::read_to_string(path)?;
125+
toml::from_str(&contents).map_err(|_| "Failed to read file".into())
126+
}
127+
128+
// Renders a template from a given string.
129+
pub fn render_template(template: &str, source_filename: &Path) -> Result<String, Error> {
130+
let template_path = source_filename.display().to_string();
131+
132+
debug!("Rendering template: {template_path}");
133+
134+
// Read the template using tera
135+
let mut tera = Tera::default();
136+
137+
// Allow templates to load variables files as Values.
138+
tera.register_function("yaml", yamlloader);
139+
tera.register_function("json", jsonloader);
140+
tera.register_function("toml", tomlloader);
141+
142+
tera.add_raw_template(&template_path, template)
143+
.map_err(|e| errors::FlokiError::ProblemRenderingTemplate {
144+
name: template_path.clone(),
145+
error: e,
146+
})?;
147+
148+
// Read the environment variables and store them in a tera context
149+
// under the `env` name.
150+
let vars: HashMap<String, String> = std::env::vars().collect();
151+
let mut context = Context::new();
152+
context.insert("env", &vars);
104153

105-
// Read the template using tera
106-
let mut tera = Tera::default();
107-
tera.add_template_file(file, Some("floki")).map_err(|e| {
154+
// Render the floki file to string using the context.
155+
Ok(tera.render(&template_path, &context)?)
156+
}
157+
158+
impl FlokiConfig {
159+
pub fn render(file: &Path) -> Result<String, Error> {
160+
let content = std::fs::read_to_string(file).map_err(|e| {
108161
errors::FlokiError::ProblemOpeningConfigYaml {
109162
name: file.display().to_string(),
110163
error: e,
111164
}
112165
})?;
113166

114-
// Read the environment variables and store them in a tera context
115-
// under the `env` name.
116-
let vars: HashMap<String, String> = std::env::vars().collect();
117-
let mut context = Context::new();
118-
context.insert("env", &vars);
167+
// Render the template first before parsing it.
168+
render_template(&content, file)
169+
}
170+
171+
pub fn from_file(file: &Path) -> Result<Self, Error> {
172+
debug!("Reading configuration file: {:?}", file);
119173

120-
// Render the floki file to string using the context.
121-
let output = tera.render("floki", &context)?;
174+
// Render the output from the configuration file before parsing.
175+
let output = Self::render(file)?;
122176

123177
// Parse the rendered floki file from the string.
124178
let mut config: FlokiConfig = serde_yaml::from_str(&output).map_err(|e| {
@@ -161,8 +215,8 @@ fn default_shell() -> Shell {
161215
Shell::Shell("sh".into())
162216
}
163217

164-
fn default_mount() -> path::PathBuf {
165-
path::Path::new("/src").to_path_buf()
218+
fn default_mount() -> PathBuf {
219+
Path::new("/src").to_path_buf()
166220
}
167221

168222
fn default_to_false() -> bool {
@@ -176,9 +230,6 @@ fn default_entrypoint() -> Entrypoint {
176230
#[cfg(test)]
177231
mod test {
178232
use super::*;
179-
use crate::image::Image;
180-
use std::io::Write;
181-
use tempfile::NamedTempFile;
182233

183234
#[derive(Debug, PartialEq, Serialize, Deserialize)]
184235
struct TestShellConfig {
@@ -264,19 +315,37 @@ mod test {
264315
}
265316

266317
#[test]
267-
fn test_tera_file() -> Result<(), Box<dyn std::error::Error>> {
268-
let yaml = r#"{% set var = "test" %}image: {{ var }}"#;
318+
fn test_tera_render() -> Result<(), Box<dyn std::error::Error>> {
319+
let template = r#"{% set var = "test" %}image: {{ var }}"#;
320+
let config = render_template(template, Path::new("floki"))?;
321+
assert_eq!(config, "image: test");
322+
Ok(())
323+
}
269324

270-
// Write to a temporary file.
271-
let mut tmp = NamedTempFile::new()?;
272-
tmp.write_all(yaml.as_bytes())?;
273-
let (_file, path) = tmp.keep()?;
325+
#[test]
326+
fn test_tera_yamlload() -> Result<(), Box<dyn std::error::Error>> {
327+
let template =
328+
r#"{% set values = yaml(file="test_resources/values.yaml") %}shell: {{ values.foo }}"#;
329+
let config = render_template(template, Path::new("floki"))?;
330+
assert_eq!(config, "shell: bar");
331+
Ok(())
332+
}
274333

275-
// Let's try and parse the file.
276-
let config = FlokiConfig::from_file(&path)?;
334+
#[test]
335+
fn test_tera_jsonload() -> Result<(), Box<dyn std::error::Error>> {
336+
let template =
337+
r#"{% set values = json(file="test_resources/values.json") %}shell: {{ values.foo }}"#;
338+
let config = render_template(template, Path::new("floki"))?;
339+
assert_eq!(config, "shell: bar");
340+
Ok(())
341+
}
277342

278-
println!("Config: {:?}", config);
279-
assert_eq!(config.image, Image::Name("test".to_string()));
343+
#[test]
344+
fn test_tera_tomlload() -> Result<(), Box<dyn std::error::Error>> {
345+
let template =
346+
r#"{% set values = toml(file="Cargo.toml") %}floki: {{ values.package.name }}"#;
347+
let config = render_template(template, Path::new("floki"))?;
348+
assert_eq!(config, "floki: floki");
280349
Ok(())
281350
}
282351
}

src/errors.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ pub enum FlokiError {
1919
#[error("Could not normalize the file path '{name}': {error:?}")]
2020
ProblemNormalizingFilePath { name: String, error: io::Error },
2121

22+
#[error("There was a problem rendering the template '{name}': {error:?}")]
23+
ProblemRenderingTemplate { name: String, error: tera::Error },
24+
2225
#[error("There was a problem opening the configuration file '{name}': {error:?}")]
23-
ProblemOpeningConfigYaml { name: String, error: tera::Error },
26+
ProblemOpeningConfigYaml { name: String, error: std::io::Error },
2427

2528
#[error("There was a problem parsing the configuration file '{name}': {error:?}")]
2629
ProblemParsingConfigYaml {

src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ fn run_floki_from_args(args: &Cli) -> Result<(), Error> {
6363
Ok(())
6464
}
6565

66+
Some(Subcommand::Render {}) => {
67+
let env = Environment::gather(&args.config_file)?;
68+
let contents = FlokiConfig::render(&env.config_file)?;
69+
println!("{contents}");
70+
Ok(())
71+
}
72+
6673
// Launch an interactive floki shell (the default)
6774
None => {
6875
let env = Environment::gather(&args.config_file)?;

test_resources/values.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"foo": "bar"
3+
}

test_resources/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo: bar

0 commit comments

Comments
 (0)