356 lines
12 KiB
Rust
356 lines
12 KiB
Rust
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,
|
|
};
|
|
|
|
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"));
|
|
|
|
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();
|
|
|
|
if let Some(provider) = matches.get_many::<String>("secrets-provider") {
|
|
tracing::trace!("secrets-provider enabled, handling for each entry");
|
|
handle_providers(provider.cloned().collect::<Vec<_>>())?
|
|
}
|
|
|
|
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("ONE_PASSWORD_INJECT")?;
|
|
let one_password_dot_env = std::env::var("ONE_PASSWORD_DOT_ENV")?;
|
|
|
|
let injectables = one_password_inject
|
|
.split(",")
|
|
.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 &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,
|
|
"1password dotenv inject"
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(Self::OnePassword {
|
|
inject: injectables,
|
|
dotenv: if PathBuf::from(&one_password_dot_env).exists() {
|
|
Some(one_password_dot_env)
|
|
} else {
|
|
None
|
|
},
|
|
})
|
|
}
|
|
_ => 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: anyhow::Result<Vec<()>> = provider
|
|
.into_iter()
|
|
.map(|p| SecretProvider::try_from(p))
|
|
.flatten()
|
|
.map(|p| match p {
|
|
SecretProvider::OnePassword { inject, dotenv } => {
|
|
if let Some(dotenv) = dotenv {
|
|
let pairs = execute_1password_inject(&dotenv)?;
|
|
for (key, value) in pairs {
|
|
tracing::debug!(env_name = &key, "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!(
|
|
"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(())
|
|
}
|