feat: rename cuddle_cli -> cuddle
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
384
cuddle/src/cli/mod.rs
Normal file
384
cuddle/src/cli/mod.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
mod subcommands;
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use clap::Command;
|
||||
|
||||
use crate::{
|
||||
actions::CuddleAction,
|
||||
config::{CuddleConfig, CuddleFetchPolicy},
|
||||
context::{CuddleContext, CuddleTreeType},
|
||||
model::*,
|
||||
util::git::GitCommit,
|
||||
};
|
||||
|
||||
use self::subcommands::render_template::RenderTemplateCommand;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CuddleCli {
|
||||
scripts: Vec<CuddleAction>,
|
||||
variables: Vec<CuddleVariable>,
|
||||
context: Option<Arc<Mutex<Vec<CuddleContext>>>>,
|
||||
command: Option<Command>,
|
||||
tmp_dir: Option<PathBuf>,
|
||||
config: CuddleConfig,
|
||||
}
|
||||
|
||||
impl CuddleCli {
|
||||
pub fn new(
|
||||
context: Option<Arc<Mutex<Vec<CuddleContext>>>>,
|
||||
config: CuddleConfig,
|
||||
) -> anyhow::Result<CuddleCli> {
|
||||
let mut cli = CuddleCli {
|
||||
scripts: vec![],
|
||||
variables: vec![],
|
||||
context: context.clone(),
|
||||
command: None,
|
||||
tmp_dir: None,
|
||||
config,
|
||||
};
|
||||
|
||||
if let Ok(provider) = std::env::var("CUDDLE_SECRETS_PROVIDER") {
|
||||
let provider = provider
|
||||
.split(",")
|
||||
.map(|p| p.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
tracing::trace!("secrets-provider enabled, handling for each entry");
|
||||
handle_providers(provider)?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
match context {
|
||||
Some(_) => {
|
||||
tracing::debug!("build full cli");
|
||||
cli = cli
|
||||
.process_variables()
|
||||
.process_scripts()
|
||||
.process_templates()?
|
||||
.build_cli();
|
||||
}
|
||||
None => {
|
||||
tracing::debug!("build bare cli");
|
||||
cli = cli.build_bare_cli();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cli)
|
||||
}
|
||||
|
||||
fn process_variables(mut self) -> Self {
|
||||
if let Ok(context_iter) = self.context.clone().unwrap().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().unwrap().lock() {
|
||||
for ctx in context_iter.iter() {
|
||||
if let Some(scripts) = ctx.plan.scripts.clone() {
|
||||
for (name, script) in scripts {
|
||||
match &script {
|
||||
CuddleScript::Shell(shell_script) => {
|
||||
self.scripts.push(CuddleAction::new(
|
||||
script.clone(),
|
||||
ctx.path.clone(),
|
||||
name,
|
||||
shell_script.description.clone(),
|
||||
))
|
||||
}
|
||||
CuddleScript::Dagger(_) => todo!(),
|
||||
CuddleScript::Lua(l) => self.scripts.push(CuddleAction::new(
|
||||
script.clone(),
|
||||
ctx.path.clone(),
|
||||
name,
|
||||
l.description.clone(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"))?;
|
||||
match self.config.get_fetch_policy()? {
|
||||
CuddleFetchPolicy::Always => {
|
||||
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().unwrap().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)
|
||||
.arg(clap::Arg::new("secrets-provider").long("secrets-provider").env("CUDDLE_SECRETS_PROVIDER"));
|
||||
|
||||
root_cmd = subcommands::x::build_command(root_cmd, self.clone());
|
||||
root_cmd = subcommands::render_template::build_command(root_cmd);
|
||||
root_cmd = subcommands::init::build_command(root_cmd, self.clone());
|
||||
|
||||
self.command = Some(root_cmd);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn build_bare_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::init::build_command(root_cmd, self.clone());
|
||||
|
||||
self.command = Some(root_cmd);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn execute(self) -> anyhow::Result<Self> {
|
||||
if let Some(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(())
|
||||
}
|
||||
Some(("init", sub_matches)) => {
|
||||
subcommands::init::execute_init(sub_matches, self.clone())
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("could not find a match")),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SecretProvider {
|
||||
OnePassword {
|
||||
inject: Vec<String>,
|
||||
dotenv: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<String> for SecretProvider {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"1password" => {
|
||||
let one_password_inject = std::env::var("CUDDLE_ONE_PASSWORD_INJECT")
|
||||
.ok()
|
||||
.filter(|f| f.as_str() != "");
|
||||
let one_password_dot_env = std::env::var("CUDDLE_ONE_PASSWORD_DOT_ENV").ok();
|
||||
|
||||
let injectables = one_password_inject
|
||||
.unwrap_or(String::new())
|
||||
.split(",")
|
||||
.filter(|s| s.contains('='))
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// for i in &injectables {
|
||||
// if !std::path::PathBuf::from(i).exists() {
|
||||
// anyhow::bail!("1pass injectable path doesn't exist: {}", i);
|
||||
// }
|
||||
// }
|
||||
if let Some(one_password_dot_env) = &one_password_dot_env {
|
||||
if let Ok(dir) = std::env::current_dir() {
|
||||
tracing::trace!(
|
||||
current_dir = dir.display().to_string(),
|
||||
dotenv = &one_password_dot_env,
|
||||
exists = PathBuf::from(&one_password_dot_env).exists(),
|
||||
"1password dotenv inject"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self::OnePassword {
|
||||
inject: injectables,
|
||||
dotenv: if let Some(one_password_dot_env) = one_password_dot_env {
|
||||
if PathBuf::from(&one_password_dot_env).exists() {
|
||||
Some(one_password_dot_env)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
value => {
|
||||
tracing::debug!(
|
||||
"provided secrets manager doesn't match any allowed values {}",
|
||||
value
|
||||
);
|
||||
Err(anyhow::anyhow!("value is not one of supported values"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_providers(provider: Vec<String>) -> anyhow::Result<()> {
|
||||
fn execute_1password(lookup: &str) -> anyhow::Result<String> {
|
||||
let out = std::process::Command::new("op")
|
||||
.arg("read")
|
||||
.arg(lookup)
|
||||
.output()?;
|
||||
let secret = std::str::from_utf8(&out.stdout)?;
|
||||
Ok(secret.to_string())
|
||||
}
|
||||
|
||||
fn execute_1password_inject(file: &str) -> anyhow::Result<Vec<(String, String)>> {
|
||||
let out = std::process::Command::new("op")
|
||||
.arg("inject")
|
||||
.arg("--in-file")
|
||||
.arg(file)
|
||||
.output()?;
|
||||
let secrets = std::str::from_utf8(&out.stdout)?.split('\n');
|
||||
let secrets_pair = secrets
|
||||
.map(|secrets_pair| secrets_pair.split_once("="))
|
||||
.flatten()
|
||||
.map(|(key, value)| (key.to_string(), value.to_string()))
|
||||
.collect::<Vec<(String, String)>>();
|
||||
|
||||
Ok(secrets_pair)
|
||||
}
|
||||
|
||||
let res = provider
|
||||
.into_iter()
|
||||
.map(|p| SecretProvider::try_from(p))
|
||||
.collect::<anyhow::Result<Vec<_>>>();
|
||||
let res = res?;
|
||||
|
||||
let res = res
|
||||
.into_iter()
|
||||
.map(|p| match p {
|
||||
SecretProvider::OnePassword { inject, dotenv } => {
|
||||
tracing::trace!(
|
||||
inject = inject.join(","),
|
||||
dotenv = dotenv,
|
||||
"handling 1password"
|
||||
);
|
||||
if let Some(dotenv) = dotenv {
|
||||
let pairs = execute_1password_inject(&dotenv).unwrap();
|
||||
for (key, value) in pairs {
|
||||
tracing::debug!(env_name = &key, value=&value, "set var from 1password");
|
||||
std::env::set_var(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
for i in inject {
|
||||
let (env_var_name, op_lookup) = i.split_once("=").ok_or(anyhow::anyhow!(
|
||||
"CUDDLE_ONE_PASSWORD_INJECT is not a key value pair ie. key:value,key2=value2"
|
||||
))?;
|
||||
let secret = execute_1password(&op_lookup)?;
|
||||
std::env::set_var(&env_var_name, secret);
|
||||
tracing::debug!(
|
||||
env_name = &env_var_name,
|
||||
lookup = &op_lookup,
|
||||
"set var from 1password"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<()>>>();
|
||||
|
||||
let _ = res?;
|
||||
|
||||
Ok(())
|
||||
}
|
218
cuddle/src/cli/subcommands/init.rs
Normal file
218
cuddle/src/cli/subcommands/init.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{create_dir_all, read, read_dir};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{ArgMatches, Command};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::cli::CuddleCli;
|
||||
|
||||
pub fn build_command(root_cmd: Command, _cli: CuddleCli) -> Command {
|
||||
let mut repo_url = clap::Arg::new("repo").long("repo").short('r');
|
||||
|
||||
if let Ok(cuddle_template_url) = std::env::var("CUDDLE_TEMPLATE_URL") {
|
||||
repo_url = repo_url.default_value(cuddle_template_url);
|
||||
} else {
|
||||
repo_url = repo_url.required(true);
|
||||
}
|
||||
|
||||
let execute_cmd = Command::new("init")
|
||||
.about("init bootstraps a repository from a template")
|
||||
.arg(repo_url)
|
||||
.arg(clap::Arg::new("name").long("name"))
|
||||
.arg(clap::Arg::new("path").long("path"))
|
||||
.arg(clap::Arg::new("value").short('v').long("value"));
|
||||
|
||||
root_cmd.subcommand(execute_cmd)
|
||||
}
|
||||
|
||||
pub fn execute_init(exe_submatch: &ArgMatches, _cli: CuddleCli) -> anyhow::Result<()> {
|
||||
let repo = exe_submatch.get_one::<String>("repo").unwrap();
|
||||
let name = exe_submatch.get_one::<String>("name");
|
||||
let path = exe_submatch.get_one::<String>("path");
|
||||
let values = exe_submatch
|
||||
.get_many::<String>("value")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tracing::info!("Downloading: {}", repo);
|
||||
|
||||
create_dir_all(std::env::temp_dir())?;
|
||||
|
||||
let tmpdir = tempfile::tempdir()?;
|
||||
|
||||
let tmpdir_path = tmpdir.path().canonicalize()?;
|
||||
|
||||
let output = std::process::Command::new("git")
|
||||
.args(&["clone", repo, "."])
|
||||
.current_dir(tmpdir_path)
|
||||
.output()?;
|
||||
|
||||
std::io::stdout().write_all(&output.stdout)?;
|
||||
std::io::stderr().write_all(&output.stderr)?;
|
||||
|
||||
let templates_path = tmpdir.path().join("cuddle-templates.json");
|
||||
let template_path = tmpdir.path().join("cuddle-template.json");
|
||||
let templates = if templates_path.exists() {
|
||||
let templates = read(templates_path)?;
|
||||
let templates: CuddleTemplates = serde_json::from_slice(&templates)?;
|
||||
|
||||
let mut single_templates = Vec::new();
|
||||
|
||||
for template_name in templates.templates.iter() {
|
||||
let template = read(
|
||||
tmpdir
|
||||
.path()
|
||||
.join(template_name)
|
||||
.join("cuddle-template.json"),
|
||||
)?;
|
||||
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
|
||||
|
||||
single_templates.push((template_name, template))
|
||||
}
|
||||
|
||||
single_templates
|
||||
.into_iter()
|
||||
.map(|(name, template)| (name.clone(), tmpdir.path().join(name), template))
|
||||
.collect::<Vec<_>>()
|
||||
} else if template_path.exists() {
|
||||
let template = read(template_path)?;
|
||||
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
|
||||
vec![(template.clone().name, tmpdir.path().to_path_buf(), template)]
|
||||
} else {
|
||||
anyhow::bail!("No cuddle-template.json or cuddle-templates.json found");
|
||||
};
|
||||
|
||||
let template = match name {
|
||||
Some(name) => {
|
||||
let template = read(tmpdir.path().join(name).join("cuddle-template.json"))?;
|
||||
let template = serde_json::from_slice::<CuddleTemplate>(&template)?;
|
||||
Ok((name.clone(), tmpdir.path().join(name), template))
|
||||
}
|
||||
None => {
|
||||
if templates.len() > 1 {
|
||||
let name = inquire::Select::new(
|
||||
"template",
|
||||
templates.iter().map(|t| t.0.clone()).collect(),
|
||||
)
|
||||
.with_help_message("name of which template to use")
|
||||
.prompt()?;
|
||||
|
||||
let found_template = templates
|
||||
.iter()
|
||||
.find(|item| item.0 == name)
|
||||
.ok_or(anyhow::anyhow!("could not find an item with that name"))?;
|
||||
|
||||
Ok(found_template.clone())
|
||||
} else if templates.len() == 1 {
|
||||
Ok(templates[0].clone())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("No templates found, with any valid names"))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (_name, template_dir, mut template) = template?;
|
||||
|
||||
let path = match path {
|
||||
Some(path) => path.clone(),
|
||||
None => inquire::Text::new("path")
|
||||
.with_help_message("to where it should be placed")
|
||||
.with_default(".")
|
||||
.prompt()?,
|
||||
};
|
||||
|
||||
create_dir_all(&path)?;
|
||||
let dir = std::fs::read_dir(&path)?;
|
||||
if dir.count() != 0 {
|
||||
for entry in read_dir(&path)? {
|
||||
let entry = entry?;
|
||||
if entry.file_name() == ".git" {
|
||||
continue;
|
||||
} else {
|
||||
anyhow::bail!("Directory {} is not empty", &path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if let Some(ref mut prompt) = template.prompt {
|
||||
'prompt: for (name, prompt) in prompt {
|
||||
for value in &values {
|
||||
if let Some((value_name, value_content)) = value.split_once("=") {
|
||||
if value_name == name {
|
||||
prompt.value = value_content.to_string();
|
||||
continue 'prompt;
|
||||
}
|
||||
}
|
||||
}
|
||||
let value = inquire::Text::new(&name)
|
||||
.with_help_message(&prompt.description)
|
||||
.prompt()?;
|
||||
prompt.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for entry in WalkDir::new(&template_dir).follow_links(false) {
|
||||
let entry = entry?;
|
||||
let entry_path = entry.path();
|
||||
|
||||
let new_path = PathBuf::from(&path).join(entry_path.strip_prefix(&template_dir)?);
|
||||
let new_path = replace_with_variables(&new_path.to_string_lossy().to_string(), &template)?;
|
||||
let new_path = PathBuf::from(new_path);
|
||||
|
||||
if entry_path.is_dir() {
|
||||
create_dir_all(&new_path)?;
|
||||
}
|
||||
|
||||
if entry_path.is_file() {
|
||||
let name = entry.file_name();
|
||||
if let Some(parent) = entry_path.parent() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
if name == "cuddle-template.json" || name == "cuddle-templates.json" {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!("writing to: {}", new_path.display());
|
||||
let new_content =
|
||||
replace_with_variables(&std::fs::read_to_string(entry_path)?, &template)?;
|
||||
|
||||
std::fs::write(new_path, new_content.as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn replace_with_variables(content: &str, template: &CuddleTemplate) -> anyhow::Result<String> {
|
||||
let mut content = content.to_string();
|
||||
if let Some(prompt) = &template.prompt {
|
||||
for (name, value) in prompt {
|
||||
content = content.replace(&format!("%%{}%%", name), &value.value);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
struct CuddleTemplates {
|
||||
pub templates: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
struct CuddleTemplate {
|
||||
pub name: String,
|
||||
pub prompt: Option<BTreeMap<String, CuddleTemplatePrompt>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
struct CuddleTemplatePrompt {
|
||||
pub description: String,
|
||||
#[serde(skip)]
|
||||
pub value: String,
|
||||
}
|
3
cuddle/src/cli/subcommands/mod.rs
Normal file
3
cuddle/src/cli/subcommands/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod init;
|
||||
pub mod render_template;
|
||||
pub mod x;
|
150
cuddle/src/cli/subcommands/render_template.rs
Normal file
150
cuddle/src/cli/subcommands/render_template.rs
Normal 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(root_cmd: Command) -> Command {
|
||||
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)),
|
||||
}
|
||||
}
|
88
cuddle/src/cli/subcommands/x.rs
Normal file
88
cuddle/src/cli/subcommands/x.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
|
||||
use crate::cli::CuddleCli;
|
||||
|
||||
pub fn build_command(root_cmd: Command, cli: CuddleCli) -> Command {
|
||||
if cli.scripts.len() > 0 {
|
||||
let execute_cmd_about = "x is your entry into your domains scripts, scripts inherited from parents will also be present here";
|
||||
let mut execute_cmd = Command::new("x")
|
||||
.about(execute_cmd_about)
|
||||
.subcommand_required(true);
|
||||
|
||||
execute_cmd = execute_cmd.subcommands(&build_scripts(cli));
|
||||
|
||||
root_cmd.subcommand(execute_cmd)
|
||||
} else {
|
||||
root_cmd
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_scripts(cli: CuddleCli) -> Vec<Command> {
|
||||
let mut cmds = Vec::new();
|
||||
for script in cli.scripts.iter() {
|
||||
let mut cmd = Command::new(&script.name);
|
||||
|
||||
if let Some(desc) = &script.description {
|
||||
cmd = cmd.about(desc)
|
||||
}
|
||||
|
||||
match &script.script {
|
||||
crate::model::CuddleScript::Shell(shell_script) => {
|
||||
if let Some(args) = &shell_script.args {
|
||||
for (arg_name, arg) in args {
|
||||
cmd = match arg {
|
||||
crate::model::CuddleShellScriptArg::Env(arg_env) => cmd.arg(
|
||||
Arg::new(arg_name.clone())
|
||||
.env(arg_name.to_uppercase().replace(".", "_"))
|
||||
.required(true),
|
||||
),
|
||||
crate::model::CuddleShellScriptArg::Flag(arg_flag) => {
|
||||
let mut arg_val = Arg::new(arg_name.clone())
|
||||
.env(arg_name.to_uppercase().replace(".", "_"))
|
||||
.long(arg_name);
|
||||
|
||||
if let Some(true) = arg_flag.required {
|
||||
arg_val = arg_val.required(true);
|
||||
}
|
||||
|
||||
if let Some(def) = &arg_flag.default_value {
|
||||
arg_val = arg_val.default_value(def);
|
||||
}
|
||||
|
||||
if let Some(desc) = &arg_flag.description {
|
||||
arg_val = arg_val.help(&*desc.clone().leak())
|
||||
}
|
||||
|
||||
cmd.arg(arg_val)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::model::CuddleScript::Dagger(_) => todo!(),
|
||||
crate::model::CuddleScript::Lua(l) => {}
|
||||
}
|
||||
|
||||
cmds.push(cmd)
|
||||
}
|
||||
|
||||
cmds
|
||||
}
|
||||
|
||||
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(action_matches, cli.variables.clone())?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("could not find a match")),
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("could not find a match")),
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user