use std::{
collections::HashMap,
env, fs,
path::{Path, PathBuf},
process,
};
use anyhow::Result;
use clap::Args;
use log::{debug, error, info, trace};
use tera::{Context, Tera};
use typed_builder::TypedBuilder;
use crate::module_recipe::Recipe;
pub const DEFAULT_CONTAINERFILE: &str = include_str!("../templates/Containerfile.tera");
pub const EXPORT_SCRIPT: &str = include_str!("../templates/export.sh");
#[derive(Debug, Clone, Args, TypedBuilder)]
pub struct TemplateCommand {
#[arg()]
recipe: PathBuf,
#[arg(short, long)]
#[builder(default, setter(into))]
containerfile: Option<PathBuf>,
#[arg(short, long)]
#[builder(default, setter(into))]
output: Option<PathBuf>,
}
impl TemplateCommand {
pub fn try_run(&self) -> Result<()> {
info!("Templating for recipe at {}", self.recipe.display());
self.template_file()
}
pub fn run(&self) {
if let Err(e) = self.try_run() {
error!("Failed to template file: {e}");
process::exit(1);
}
}
fn template_file(&self) -> Result<()> {
trace!("TemplateCommand::template_file()");
debug!("Setting up tera");
let (tera, context) = self.setup_tera()?;
trace!("tera: {tera:#?}");
trace!("context: {context:#?}");
debug!("Rendering Containerfile");
let output_str = tera.render("Containerfile", &context)?;
match self.output.as_ref() {
Some(output) => {
debug!("Templating to file {}", output.display());
trace!("Containerfile:\n{output_str}");
std::fs::write(output, output_str)?;
}
None => {
debug!("Templating to stdout");
println!("{output_str}");
}
}
info!("Finished templating Containerfile");
Ok(())
}
fn setup_tera(&self) -> Result<(Tera, Context)> {
trace!("TemplateCommand::setup_tera()");
debug!("Deserializing recipe");
let recipe_de = serde_yaml::from_str::<Recipe>(fs::read_to_string(&self.recipe)?.as_str())?;
trace!("recipe_de: {recipe_de:#?}");
debug!("Building context");
let mut context = Context::from_serialize(recipe_de)?;
trace!("add to context 'recipe': {}", self.recipe.display());
context.insert("recipe", &self.recipe);
let mut tera = Tera::default();
match self.containerfile.as_ref() {
Some(containerfile) => {
debug!("Using {} as the template", containerfile.display());
tera.add_raw_template("Containerfile", &fs::read_to_string(containerfile)?)?
}
None => tera.add_raw_template("Containerfile", DEFAULT_CONTAINERFILE)?,
}
debug!("Registering function `print_containerfile`");
tera.register_function(
"print_containerfile",
|args: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
trace!("tera fn print_containerfile({args:#?})");
match args.get("containerfile") {
Some(v) => match v.as_str() {
Some(containerfile) => {
debug!("Loading containerfile contents for {containerfile}");
let path =
format!("config/containerfiles/{containerfile}/Containerfile");
let path = Path::new(path.as_str());
let file = fs::read_to_string(path)?;
trace!("Containerfile contents {}:\n{file}", path.display());
Ok(file.into())
}
None => Err("Arg containerfile wasn't a string".into()),
},
None => {
Err("Needs the argument 'containerfile' for print_containerfile()".into())
}
}
},
);
debug!("Registering function `print_module_context`");
tera.register_function(
"print_module_context",
|args: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
trace!("tera fn print_module_context({args:#?})");
match args.get("module") {
Some(v) => match serde_json::to_string(v) {
Ok(s) => Ok(s.into()),
Err(e) => Err(format!("Unable to serialize: {e}").into()),
},
None => Err("Needs the argument 'module' for print_module_context()".into()),
}
},
);
debug!("Registering function `get_module_from_file`");
tera.register_function(
"get_module_from_file",
|args: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
trace!("tera fn get_module_from_file({args:#?})");
match args.get("file") {
Some(v) => {
let file = match v.as_str() {
Some(s) => s,
None => return Err("Property 'from-file' must be a string".into()),
};
trace!("from-file: {file}");
match serde_yaml::from_str::<tera::Value>(
fs::read_to_string(format!("config/{file}"))?.as_str(),
) {
Ok(context) => {
trace!("context: {context}");
Ok(context)
}
Err(_) => Err(format!("Unable to deserialize file {file}").into()),
}
}
None => Err("Needs the argument 'file' for get_module_from_file()".into()),
}
},
);
debug!("Registering function `running_gitlab_actions`");
tera.register_function(
"running_gitlab_actions",
|_: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
trace!("tera fn running_gitlab_actions()");
Ok(env::var("GITHUB_ACTIONS").is_ok_and(|e| e == "true").into())
},
);
debug!("Registering function `print_script`");
tera.register_function(
"print_script",
|args: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
trace!("tera fn print_script({args:#?})");
let escape_script = |script_contents: &str| {
format!(
"\"{}\"",
script_contents
.replace('\n', "\\n")
.replace('\"', "\\\"")
.replace('$', "\\$")
)
};
match args.get("script") {
Some(x) => match x.as_str().unwrap_or_default() {
"export" => Ok(escape_script(EXPORT_SCRIPT).into()),
_ => Err(format!("Script {x} doesn't exist").into()),
},
None => Err("Needs the argument 'script' for 'print_script()'".into()),
}
},
);
Ok((tera, context))
}
}