use std::collections::HashMap;
use std::path::Path;
use serde::Deserialize;
use wax::walk::{Entry as _, GlobEntry};
use super::Pipe;
use super::{Error, Result};
#[serde_with::serde_as]
#[derive(Debug, Deserialize)]
pub struct Action {
#[serde(default)]
engine: Engine,
#[serde_as(as = "Option<serde_with::DisplayFromStr>")]
glob: Option<String>,
variables: HashMap<String, String>,
}
impl Action {
pub async fn execute(&self, pipe: &mut Pipe) -> Result<()> {
let input = pipe.input().ok_or(Error::NoInputInPipe)?;
let output = pipe.output_temp()?;
tracing::info!(
"Rendering template: {} -> {}",
input.display(),
output.display(),
);
crate::util::fs::copy_dir(input.clone(), output.clone()).await?;
let pattern = self.glob.as_deref().unwrap_or("**/*");
let glob = wax::Glob::new(pattern)?;
let walker = glob
.walk(&input)
.filter_map(std::result::Result::ok)
.filter(|entry| entry.file_type().is_file());
match self.engine {
Engine::Tera => self.generate_tera(&input, &output, pattern, walker).await,
}
}
async fn generate_tera(
&self,
input: &Path,
output: &Path,
pattern: &str,
walker: impl Iterator<Item = GlobEntry>,
) -> Result<()> {
use tera::{Context, Tera};
let templates_dir = format!("{input}/{pattern}", input = input.display());
let mut tera = Tera::new();
tera.load_from_glob(&templates_dir)?;
let mut context = Context::new();
for (key, value) in &self.variables {
let value = shellexpand::env(value)?;
context.insert(key.clone(), &value);
}
for entry in walker {
let rel_path = entry.path().strip_prefix(input).unwrap();
let output_path = output.join(rel_path);
if output_path.exists() {
std::fs::remove_file(&output_path).map_err(|err| Error::CouldNotOpenFile {
path: output_path.clone(),
err,
})?;
}
let output_file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&output_path)
.map_err(|err| Error::CouldNotOpenFile {
path: output_path,
err,
})?;
tera.render_to(&rel_path.to_string_lossy(), &context, output_file)?;
}
Ok(())
}
}
#[derive(Debug, Default, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Engine {
#[default]
#[serde(alias = "jinja2")]
Tera,
}
#[cfg(test)]
mod tests {
use super::Action;
use crate::ops::run::codegen::actions::Pipe;
fn action(spec: &str) -> Action {
serde_yaml_ng::from_str(spec).unwrap()
}
fn pipe_from(input: &std::path::Path) -> Pipe {
let mut pipe = Pipe::new();
pipe.output_path(input);
pipe.pipe()
}
#[tokio::test]
async fn renders_a_tree_into_the_next_step() {
let input = tempfile::TempDir::with_prefix("adnv-tpl-in").unwrap();
std::fs::write(input.path().join("values.yaml"), "name: {{ name }}\n").unwrap();
let mut pipe = pipe_from(input.path());
action("variables:\n name: mattermost\n")
.execute(&mut pipe)
.await
.expect("a tree without links renders");
let piped = pipe.pipe();
let output = piped.input().unwrap();
assert_eq!(
std::fs::read_to_string(output.join("values.yaml")).unwrap(),
"name: mattermost\n"
);
}
#[tokio::test]
async fn refuses_a_tree_holding_a_link_before_reading_through_it() {
let outside = tempfile::TempDir::with_prefix("adnv-tpl-out").unwrap();
let secret = outside.path().join("secret");
std::fs::write(&secret, "INTACT").unwrap();
let input = tempfile::TempDir::with_prefix("adnv-tpl-in").unwrap();
std::fs::write(input.path().join("values.yaml"), "name: {{ name }}\n").unwrap();
std::os::unix::fs::symlink(&secret, input.path().join("link")).unwrap();
let mut pipe = pipe_from(input.path());
let err = action("variables:\n name: mattermost\n")
.execute(&mut pipe)
.await
.expect_err("a tree holding a link is refused");
assert!(
matches!(err, crate::Error::SymlinkInSource { .. }),
"{err:?}"
);
assert_eq!(
std::fs::read_to_string(&secret).unwrap(),
"INTACT",
"the link was written through"
);
}
}