without autoescape

This commit is contained in:
2022-08-14 20:19:29 +02:00
parent 49089b3074
commit 319360968b
17 changed files with 1083 additions and 332 deletions

View File

@@ -18,3 +18,4 @@ tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["json"] }
log = { version = "0.4.17", features = ["std", "kv_unstable"] }
openssl = {version = "0.10", features = ["vendored"]}
tera = "1.17.0"

View File

@@ -1 +1,78 @@
use std::path::PathBuf;
use crate::{
actions::shell::ShellAction,
model::{CuddleScript, CuddleShellScriptArg, CuddleVariable},
};
pub mod shell;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CuddleAction {
pub script: CuddleScript,
pub path: PathBuf,
pub name: String,
}
#[allow(dead_code)]
impl CuddleAction {
pub fn new(script: CuddleScript, path: PathBuf, name: String) -> Self {
Self { script, path, name }
}
pub fn execute(self, variables: Vec<CuddleVariable>) -> anyhow::Result<()> {
match self.script {
CuddleScript::Shell(s) => {
let mut arg_variables: Vec<CuddleVariable> = vec![];
if let Some(args) = s.args {
for (k, v) in args {
let var = match v {
CuddleShellScriptArg::Env(e) => {
let env_var = match std::env::var(e.key.clone()) {
Ok(var) => var,
Err(e) => {
log::error!("env_variable not found: {}", k);
return Err(anyhow::anyhow!(e));
}
};
CuddleVariable::new(k.clone(), env_var)
}
};
arg_variables.push(var);
}
} else {
arg_variables = vec![]
};
let mut vars = variables.clone();
vars.append(&mut arg_variables);
log::trace!("preparing to run action");
match ShellAction::new(
self.name.clone(),
format!(
"{}/scripts/{}.sh",
self.path
.to_str()
.expect("action doesn't have a name, this should never happen"),
self.name
),
)
.execute(vars)
{
Ok(()) => {
log::trace!("finished running action");
Ok(())
}
Err(e) => {
log::error!("{}", e);
Err(e)
}
}
}
CuddleScript::Dagger(_d) => Err(anyhow::anyhow!("not implemented yet!")),
}
}
}

View File

@@ -1,6 +1,6 @@
use std::{env::current_dir, path::PathBuf, process::Command};
use crate::cli::CuddleVariable;
use crate::model::CuddleVariable;
#[allow(dead_code)]
pub struct ShellAction {

View File

@@ -1,314 +0,0 @@
use std::{
env::{self, current_dir},
path::PathBuf,
sync::{Arc, Mutex},
};
use anyhow::anyhow;
use clap::Command;
use git2::Repository;
use crate::{
actions,
context::{CuddleContext, CuddleTreeType},
model::{CuddleScript, CuddleShellScriptArg},
};
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct CuddleAction {
script: CuddleScript,
path: PathBuf,
name: String,
}
#[allow(dead_code)]
impl CuddleAction {
pub fn new(script: CuddleScript, path: PathBuf, name: String) -> Self {
Self { script, path, name }
}
pub fn execute(self, variables: Vec<CuddleVariable>) -> anyhow::Result<()> {
match self.script {
CuddleScript::Shell(s) => {
let mut arg_variables: Vec<CuddleVariable> = vec![];
if let Some(args) = s.args {
for (k, v) in args {
let var = match v {
CuddleShellScriptArg::Env(e) => {
let env_var = match env::var(e.key.clone()) {
Ok(var) => var,
Err(e) => {
log::error!("env_variable not found: {}", k);
return Err(anyhow::anyhow!(e));
}
};
CuddleVariable::new(k.clone(), env_var)
}
};
arg_variables.push(var);
}
} else {
arg_variables = vec![]
};
let mut vars = variables.clone();
vars.append(&mut arg_variables);
log::trace!("preparing to run action");
match actions::shell::ShellAction::new(
self.name.clone(),
format!(
"{}/scripts/{}.sh",
self.path
.to_str()
.expect("action doesn't have a name, this should never happen"),
self.name
),
)
.execute(vars)
{
Ok(()) => {
log::trace!("finished running action");
Ok(())
}
Err(e) => {
log::error!("{}", e);
Err(e)
}
}
}
CuddleScript::Dagger(_d) => Err(anyhow::anyhow!("not implemented yet!")),
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CuddleVariable {
pub name: String,
pub value: String,
}
impl CuddleVariable {
pub fn new(name: String, value: String) -> Self {
Self { name, value }
}
}
#[derive(Debug)]
struct GitCommit {
commit_sha: String,
}
impl GitCommit {
fn new() -> anyhow::Result<GitCommit> {
let repo = Repository::open(current_dir().expect("having current_dir available")).map_err(
|e| {
log::debug!("{}", e);
anyhow::anyhow!("could not open repository")
},
)?;
let head_ref = repo
.head()
.map_err(|e| {
log::warn!("{}", e);
anyhow::anyhow!("could not get HEAD")
})?
.target()
.ok_or(anyhow::anyhow!(
"could not extract head -> target to commit_sha"
))?;
Ok(Self {
commit_sha: head_ref.to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct CuddleCli<'a> {
scripts: Vec<CuddleAction>,
variables: Vec<CuddleVariable>,
context: Arc<Mutex<Vec<CuddleContext>>>,
command: Option<Command<'a>>,
tmp_dir: Option<PathBuf>,
}
impl<'a> CuddleCli<'a> {
pub fn new(context: Arc<Mutex<Vec<CuddleContext>>>) -> anyhow::Result<CuddleCli<'a>> {
let mut cli = CuddleCli {
scripts: vec![],
variables: vec![],
context: context.clone(),
command: None,
tmp_dir: None,
};
cli = cli
.process_variables()
.process_scripts()
.process_templates()?
.build_cli();
Ok(cli)
}
fn process_variables(mut self) -> Self {
if let Ok(context_iter) = self.context.clone().lock() {
for ctx in context_iter.iter() {
if let Some(variables) = ctx.plan.vars.clone() {
for (name, var) in variables {
self.variables.push(CuddleVariable::new(name, var))
}
}
if let CuddleTreeType::Root = ctx.node_type {
let mut temp_path = ctx.path.clone();
temp_path.push(".cuddle/tmp/");
self.variables.push(CuddleVariable::new(
"tmp".into(),
temp_path.clone().to_string_lossy().to_string(),
));
self.tmp_dir = Some(temp_path);
}
}
}
match GitCommit::new() {
Ok(commit) => self.variables.push(CuddleVariable::new(
"commit_sha".into(),
commit.commit_sha.clone(),
)),
Err(e) => {
log::debug!("{}", e);
}
}
self
}
fn process_scripts(mut self) -> Self {
if let Ok(context_iter) = self.context.clone().lock() {
for ctx in context_iter.iter() {
if let Some(scripts) = ctx.plan.scripts.clone() {
for (name, script) in scripts {
self.scripts
.push(CuddleAction::new(script.clone(), ctx.path.clone(), name))
}
}
}
}
self
}
fn process_templates(self) -> anyhow::Result<Self> {
if let None = self.tmp_dir {
log::debug!("cannot process template as bare bones cli");
return Ok(self);
}
// Make sure tmp_dir exists and clean it up first
let tmp_dir = self
.tmp_dir
.clone()
.ok_or(anyhow::anyhow!("tmp_dir does not exist aborting"))?;
if tmp_dir.exists() && tmp_dir.ends_with("tmp") {
std::fs::remove_dir_all(tmp_dir.clone())?;
}
std::fs::create_dir_all(tmp_dir.clone())?;
// Handle all templating with variables and such.
// TODO: use actual templating engine, for new we just copy templates to the final folder
if let Ok(context_iter) = self.context.clone().lock() {
for ctx in context_iter.iter() {
let mut template_path = ctx.path.clone();
template_path.push("templates");
log::trace!("template path: {}", template_path.clone().to_string_lossy());
if !template_path.exists() {
continue;
}
for file in std::fs::read_dir(template_path)?.into_iter() {
let f = file?;
let mut dest_file = tmp_dir.clone();
dest_file.push(f.file_name());
std::fs::copy(f.path(), dest_file)?;
}
}
}
Ok(self)
}
fn build_cli(mut self) -> Self {
let mut root_cmd = Command::new("cuddle")
.version("1.0")
.author("kjuulh <contact@kasperhermansen.com>")
.about("cuddle is your domain specific organization tool. It enabled widespread sharing through repositories, as well as collaborating while maintaining speed and integrity")
.subcommand_required(true)
.propagate_version(true);
if self.scripts.len() > 0 {
let mut execute_cmd = Command::new("x").about("x is your entry into your domains scripts, scripts inherited from parents will also be present here").subcommand_required(true);
for script in self.scripts.iter() {
let action_cmd = Command::new(script.name.clone());
// TODO: Some way to add an about for clap, requires conversion from String -> &str
execute_cmd = execute_cmd.subcommand(action_cmd);
}
root_cmd = root_cmd.subcommand(execute_cmd);
}
self.command = Some(root_cmd);
self
}
pub fn execute(self) -> anyhow::Result<Self> {
if let Some(mut cli) = self.command.clone() {
let matches = cli.clone().get_matches();
let res = match matches.subcommand() {
Some(("x", exe_submatch)) => {
log::trace!("executing x");
match exe_submatch.subcommand() {
Some((name, _action_matches)) => {
log::trace!(action=name; "running action; name={}", name);
match self.scripts.iter().find(|ele| ele.name == name) {
Some(script) => {
script.clone().execute(self.variables.clone())?;
Ok(())
}
_ => Err(anyhow!("could not find a match")),
}
}
_ => Err(anyhow!("could not find a match")),
}
}
_ => Err(anyhow!("could not find a match")),
};
match res {
Ok(()) => {}
Err(e) => {
let _ = cli.print_long_help();
return Err(e);
}
}
}
Ok(self)
}
}

182
cuddle_cli/src/cli/mod.rs Normal file
View File

@@ -0,0 +1,182 @@
mod subcommands;
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use clap::Command;
use crate::{
actions::CuddleAction,
context::{CuddleContext, CuddleTreeType},
model::*,
util::git::GitCommit,
};
use self::subcommands::render_template::RenderTemplateCommand;
#[derive(Debug, Clone)]
pub struct CuddleCli<'a> {
scripts: Vec<CuddleAction>,
variables: Vec<CuddleVariable>,
context: Arc<Mutex<Vec<CuddleContext>>>,
command: Option<Command<'a>>,
tmp_dir: Option<PathBuf>,
}
impl<'a> CuddleCli<'a> {
pub fn new(context: Arc<Mutex<Vec<CuddleContext>>>) -> anyhow::Result<CuddleCli<'a>> {
let mut cli = CuddleCli {
scripts: vec![],
variables: vec![],
context: context.clone(),
command: None,
tmp_dir: None,
};
cli = cli
.process_variables()
.process_scripts()
.process_templates()?
.build_cli();
Ok(cli)
}
fn process_variables(mut self) -> Self {
if let Ok(context_iter) = self.context.clone().lock() {
for ctx in context_iter.iter() {
if let Some(variables) = ctx.plan.vars.clone() {
for (name, var) in variables {
self.variables.push(CuddleVariable::new(name, var))
}
}
if let CuddleTreeType::Root = ctx.node_type {
let mut temp_path = ctx.path.clone();
temp_path.push(".cuddle/tmp");
self.variables.push(CuddleVariable::new(
"tmp".into(),
temp_path.clone().to_string_lossy().to_string(),
));
self.tmp_dir = Some(temp_path);
}
}
}
match GitCommit::new() {
Ok(commit) => self.variables.push(CuddleVariable::new(
"commit_sha".into(),
commit.commit_sha.clone(),
)),
Err(e) => {
log::debug!("{}", e);
}
}
self
}
fn process_scripts(mut self) -> Self {
if let Ok(context_iter) = self.context.clone().lock() {
for ctx in context_iter.iter() {
if let Some(scripts) = ctx.plan.scripts.clone() {
for (name, script) in scripts {
self.scripts
.push(CuddleAction::new(script.clone(), ctx.path.clone(), name))
}
}
}
}
self
}
fn process_templates(self) -> anyhow::Result<Self> {
if let None = self.tmp_dir {
log::debug!("cannot process template as bare bones cli");
return Ok(self);
}
// Make sure tmp_dir exists and clean it up first
let tmp_dir = self
.tmp_dir
.clone()
.ok_or(anyhow::anyhow!("tmp_dir does not exist aborting"))?;
if tmp_dir.exists() && tmp_dir.ends_with("tmp") {
std::fs::remove_dir_all(tmp_dir.clone())?;
}
std::fs::create_dir_all(tmp_dir.clone())?;
// Handle all templating with variables and such.
// TODO: use actual templating engine, for new we just copy templates to the final folder
if let Ok(context_iter) = self.context.clone().lock() {
for ctx in context_iter.iter() {
let mut template_path = ctx.path.clone();
template_path.push("templates");
log::trace!("template path: {}", template_path.clone().to_string_lossy());
if !template_path.exists() {
continue;
}
for file in std::fs::read_dir(template_path)?.into_iter() {
let f = file?;
let mut dest_file = tmp_dir.clone();
dest_file.push(f.file_name());
std::fs::copy(f.path(), dest_file)?;
}
}
}
Ok(self)
}
fn build_cli(mut self) -> Self {
let mut root_cmd = Command::new("cuddle")
.version("1.0")
.author("kjuulh <contact@kasperhermansen.com>")
.about("cuddle is your domain specific organization tool. It enabled widespread sharing through repositories, as well as collaborating while maintaining speed and integrity")
.subcommand_required(true)
.arg_required_else_help(true)
.propagate_version(true);
root_cmd = subcommands::x::build_command(root_cmd, self.clone());
root_cmd = subcommands::render_template::build_command(root_cmd);
self.command = Some(root_cmd);
self
}
pub fn execute(self) -> anyhow::Result<Self> {
if let Some(mut cli) = self.command.clone() {
let matches = cli.clone().get_matches();
let res = match matches.subcommand() {
Some(("x", exe_submatch)) => subcommands::x::execute_x(exe_submatch, self.clone()),
Some(("render_template", sub_matches)) => {
RenderTemplateCommand::from_matches(sub_matches, self.clone())
.and_then(|cmd| cmd.execute())?;
Ok(())
}
_ => Err(anyhow::anyhow!("could not find a match")),
};
match res {
Ok(()) => {}
Err(e) => {
let _ = cli.print_long_help();
return Err(e);
}
}
}
Ok(self)
}
}

View File

@@ -0,0 +1,2 @@
pub mod render_template;
pub mod x;

View File

@@ -0,0 +1,150 @@
use std::{path::PathBuf, str::FromStr};
use clap::{Arg, ArgMatches, Command};
use crate::{cli::CuddleCli, model::CuddleVariable};
pub fn build_command<'a>(root_cmd: Command<'a>) -> Command<'a> {
root_cmd.subcommand(
Command::new("render_template")
.about("renders a jinja compatible template")
.args(&[
Arg::new("template-file")
.alias("template")
.short('t')
.long("template-file")
.required(true)
.action(clap::ArgAction::Set).long_help("template-file is the input file path of the .tmpl file (or inferred) that you would like to render"),
Arg::new("destination")
.alias("dest")
.short('d')
.long("destination")
.required(true)
.action(clap::ArgAction::Set)
.long_help("destination is the output path of the template once done, but default .tmpl is stripped and the normal file extension is used. this can be overwritten if a file path is entered instead. I.e. (/some/file/name.txt)"),
Arg::new("extra-var")
.long("extra-var")
.required(false)
.action(clap::ArgAction::Set),
]))
}
pub struct RenderTemplateCommand {
variables: Vec<CuddleVariable>,
template_file: PathBuf,
destination: PathBuf,
}
impl RenderTemplateCommand {
pub fn from_matches(matches: &ArgMatches, cli: CuddleCli) -> anyhow::Result<Self> {
let template_file = matches
.get_one::<String>("template-file")
.ok_or(anyhow::anyhow!("template-file was not found"))
.and_then(get_path_buf_and_check_exists)?;
let destination = matches
.get_one::<String>("destination")
.ok_or(anyhow::anyhow!("destination was not found"))
.and_then(get_path_buf_and_check_dir_exists)
.and_then(RenderTemplateCommand::transform_extension)?;
let mut extra_vars: Vec<CuddleVariable> =
if let Some(extra_vars) = matches.get_many::<String>("extra-var") {
let mut vars = Vec::with_capacity(extra_vars.len());
for var in extra_vars.into_iter() {
let parts: Vec<&str> = var.split("=").collect();
if parts.len() != 2 {
return Err(anyhow::anyhow!("extra-var: is not set correctly: {}", var));
}
vars.push(CuddleVariable::new(parts[0].into(), parts[1].into()));
}
vars
} else {
vec![]
};
extra_vars.append(&mut cli.variables.clone());
Ok(Self {
variables: extra_vars,
template_file,
destination,
})
}
pub fn execute(self) -> anyhow::Result<()> {
// Prepare context
let mut context = tera::Context::new();
for var in self.variables {
context.insert(
var.name.to_lowercase().replace(" ", "_").replace("-", "_"),
&var.value,
)
}
// Load source template
let source = std::fs::read_to_string(self.template_file)?;
let output = tera::Tera::one_off(source.as_str(), &context, false)?;
// Put template in final destination
std::fs::write(&self.destination, output)?;
log::info!(
"finished writing template to: {}",
&self.destination.to_string_lossy()
);
Ok(())
}
fn transform_extension(template_path: PathBuf) -> anyhow::Result<PathBuf> {
if template_path.is_file() {
let ext = template_path.extension().ok_or(anyhow::anyhow!(
"destination path does not have an extension"
))?;
if ext.to_string_lossy().ends_with("tmpl") {
let template_dest = template_path
.to_str()
.and_then(|s| s.strip_suffix(".tmpl"))
.ok_or(anyhow::anyhow!("string does not end in .tmpl"))?;
return PathBuf::from_str(template_dest).map_err(|e| anyhow::anyhow!(e));
}
}
Ok(template_path)
}
}
fn get_path_buf_and_check_exists(raw_path: &String) -> anyhow::Result<PathBuf> {
match PathBuf::from_str(&raw_path) {
Ok(pb) => {
if pb.exists() {
Ok(pb)
} else {
Err(anyhow::anyhow!(
"path: {}, could not be found",
pb.to_string_lossy()
))
}
}
Err(e) => Err(anyhow::anyhow!(e)),
}
}
fn get_path_buf_and_check_dir_exists(raw_path: &String) -> anyhow::Result<PathBuf> {
match PathBuf::from_str(&raw_path) {
Ok(pb) => {
if pb.is_dir() && pb.exists() {
Ok(pb)
} else if pb.is_file() {
Ok(pb)
} else {
Ok(pb)
}
}
Err(e) => Err(anyhow::anyhow!(e)),
}
}

View File

@@ -0,0 +1,37 @@
use clap::{ArgMatches, Command};
use crate::cli::CuddleCli;
pub fn build_command<'a>(root_cmd: Command<'a>, cli: CuddleCli<'a>) -> Command<'a> {
if cli.scripts.len() > 0 {
let mut execute_cmd = Command::new("x").about("x is your entry into your domains scripts, scripts inherited from parents will also be present here").subcommand_required(true);
for script in cli.scripts.iter() {
let action_cmd = Command::new(script.name.clone());
// TODO: Some way to add an about for clap, requires conversion from String -> &str
execute_cmd = execute_cmd.subcommand(action_cmd);
}
root_cmd.subcommand(execute_cmd)
} else {
root_cmd
}
}
pub fn execute_x(exe_submatch: &ArgMatches, cli: CuddleCli) -> anyhow::Result<()> {
match exe_submatch.subcommand() {
Some((name, _action_matches)) => {
log::trace!(action=name; "running action; name={}", name);
match cli.scripts.iter().find(|ele| ele.name == name) {
Some(script) => {
script.clone().execute(cli.variables.clone())?;
Ok(())
}
_ => Err(anyhow::anyhow!("could not find a match")),
}
}
_ => Err(anyhow::anyhow!("could not find a match")),
}
}

View File

@@ -3,6 +3,7 @@ use envconfig::Envconfig;
pub enum CuddleFetchPolicy {
Always,
Once,
Never,
}
#[derive(Envconfig, Clone)]
@@ -20,6 +21,7 @@ impl CuddleConfig {
match self.fetch_policy.clone().to_lowercase().as_str() {
"always" => Ok(CuddleFetchPolicy::Always),
"once" => Ok(CuddleFetchPolicy::Once),
"never" => Ok(CuddleFetchPolicy::Never),
_ => Err(anyhow::anyhow!("could not parse fetch policy")),
}
}

View File

@@ -1,8 +1,3 @@
use std::{
env::current_dir,
sync::{Arc, Mutex},
};
use config::CuddleConfig;
use tracing::Level;
@@ -11,23 +6,15 @@ mod cli;
mod config;
mod context;
mod model;
mod util;
fn main() -> anyhow::Result<()> {
init_logging()?;
let config = CuddleConfig::from_env()?;
match git2::Repository::open(current_dir()?) {
Ok(_) => {
let context = context::extract_cuddle(config.clone())?;
_ = cli::CuddleCli::new(context)?.execute()?;
}
Err(_) => {
// Only build bare bones cli
log::info!("was not opened in a repo with git, only showing bare-bones options");
_ = cli::CuddleCli::new(Arc::new(Mutex::new(vec![])))?.execute()?;
}
}
let context = context::extract_cuddle(config.clone())?;
_ = cli::CuddleCli::new(context)?.execute()?;
Ok(())
}
@@ -35,7 +22,7 @@ fn main() -> anyhow::Result<()> {
fn init_logging() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.pretty()
.with_max_level(Level::DEBUG)
.with_max_level(Level::INFO)
.init();
Ok(())

View File

@@ -46,3 +46,16 @@ pub struct CuddlePlan {
pub vars: Option<HashMap<String, String>>,
pub scripts: Option<HashMap<String, CuddleScript>>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CuddleVariable {
pub name: String,
pub value: String,
}
impl CuddleVariable {
pub fn new(name: String, value: String) -> Self {
Self { name, value }
}
}

View File

@@ -0,0 +1,33 @@
use std::env::current_dir;
use git2::Repository;
#[derive(Debug)]
pub struct GitCommit {
pub commit_sha: String,
}
impl GitCommit {
pub fn new() -> anyhow::Result<GitCommit> {
let repo = Repository::open(current_dir().expect("having current_dir available")).map_err(
|e| {
log::debug!("{}", e);
anyhow::anyhow!("could not open repository")
},
)?;
let head_ref = repo
.head()
.map_err(|e| {
log::warn!("{}", e);
anyhow::anyhow!("could not get HEAD")
})?
.target()
.ok_or(anyhow::anyhow!(
"could not extract head -> target to commit_sha"
))?;
Ok(Self {
commit_sha: head_ref.to_string(),
})
}
}

View File

@@ -0,0 +1 @@
pub mod git;