feat: rename cuddle_cli -> cuddle
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
42
cuddle/Cargo.toml
Normal file
42
cuddle/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "cuddle"
|
||||
description = "cuddle is a shuttle inspired script and configuration management tool. It enables sharing of workflows on developers workstations and ci"
|
||||
repository = "https://git.front.kjuulh.io/kjuulh/cuddle"
|
||||
readme = "../README.md"
|
||||
license-file = "../LICENSE"
|
||||
publish = true
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "cuddle"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.72", features = ["backtrace"] }
|
||||
serde = { version = "1.0.183", features = ["derive"] }
|
||||
serde_yaml = "0.9.25"
|
||||
walkdir = "2.3.3"
|
||||
git2 = { version = "0.17.2", default-features = false, features = [
|
||||
"vendored-libgit2",
|
||||
"vendored-openssl",
|
||||
"ssh",
|
||||
] }
|
||||
clap = { version = "4.3.21", features = ["env", "string"] }
|
||||
envconfig = "0.10.0"
|
||||
dirs = "5.0.1"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["json", "env-filter"] }
|
||||
log = { version = "0.4.20", features = ["std", "kv_unstable"] }
|
||||
tera = "1.19.0"
|
||||
openssl = { version = "0.10.56", features = ["vendored"] }
|
||||
libz-sys = { version = "1.1.12", default-features = false, features = [
|
||||
"libc",
|
||||
"static",
|
||||
] }
|
||||
inquire = { version = "0.6.2", features = ["console"] }
|
||||
tempfile = { version = "3.7.1" }
|
||||
serde_json = "1.0.104"
|
||||
rlua = "0.19.7"
|
||||
rlua-searcher = "0.1.0"
|
||||
dotenv = { version = "0.15.0", features = ["clap"] }
|
188
cuddle/src/actions/mod.rs
Normal file
188
cuddle/src/actions/mod.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::ArgMatches;
|
||||
use rlua::Lua;
|
||||
use rlua_searcher::AddSearcher;
|
||||
|
||||
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 description: Option<String>,
|
||||
pub name: String,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
impl CuddleAction {
|
||||
pub fn new(
|
||||
script: CuddleScript,
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
script,
|
||||
path,
|
||||
name,
|
||||
description,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
self,
|
||||
matches: &ArgMatches,
|
||||
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 = matches.get_one::<String>(&k).cloned().ok_or(
|
||||
anyhow::anyhow!(
|
||||
"failed to find env variable with key: {}",
|
||||
&e.key
|
||||
),
|
||||
)?;
|
||||
|
||||
CuddleVariable::new(k.clone(), env_var)
|
||||
}
|
||||
CuddleShellScriptArg::Flag(flag) => {
|
||||
match matches.get_one::<String>(&flag.name) {
|
||||
Some(flag_var) => {
|
||||
CuddleVariable::new(k.clone(), flag_var.clone())
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
arg_variables.push(var);
|
||||
}
|
||||
} else {
|
||||
arg_variables = vec![]
|
||||
};
|
||||
|
||||
let mut vars = variables.clone();
|
||||
vars.append(&mut arg_variables);
|
||||
|
||||
log::trace!("preparing to run action");
|
||||
|
||||
return 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!")),
|
||||
CuddleScript::Lua(l) => {
|
||||
let lua = Lua::new();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
//map.insert("init".into(), "print(\"something\")".into());
|
||||
|
||||
let lua_dir = PathBuf::new().join(&self.path).join("scripts").join("lua");
|
||||
if lua_dir.exists() {
|
||||
let absolute_lua_dir = lua_dir.canonicalize()?;
|
||||
for entry in walkdir::WalkDir::new(&lua_dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
if entry.metadata()?.is_file() {
|
||||
let full_file_path = entry.path().canonicalize()?;
|
||||
let relative_module_path =
|
||||
full_file_path.strip_prefix(&absolute_lua_dir)?;
|
||||
let module_path = relative_module_path
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.trim_end_matches("/init.lua")
|
||||
.trim_end_matches(".lua")
|
||||
.replace("/", ".");
|
||||
let contents = std::fs::read_to_string(entry.path())?;
|
||||
tracing::trace!(module_path = &module_path, "adding lua file");
|
||||
|
||||
map.insert(module_path.into(), contents.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
let lua_rocks_dir = PathBuf::new()
|
||||
.join(&self.path)
|
||||
.join("lua_modules")
|
||||
.join("share")
|
||||
.join("lua")
|
||||
.join("5.4");
|
||||
if lua_rocks_dir.exists() {
|
||||
let absolute_lua_dir = lua_rocks_dir.canonicalize()?;
|
||||
for entry in walkdir::WalkDir::new(&lua_rocks_dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
if entry.metadata()?.is_file() {
|
||||
let full_file_path = entry.path().canonicalize()?;
|
||||
let relative_module_path =
|
||||
full_file_path.strip_prefix(&absolute_lua_dir)?;
|
||||
let module_path = relative_module_path
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.trim_end_matches("/init.lua")
|
||||
.trim_end_matches(".lua")
|
||||
.replace("/", ".");
|
||||
let contents = std::fs::read_to_string(entry.path())?;
|
||||
tracing::trace!(module_path = &module_path, "adding lua file");
|
||||
|
||||
map.insert(module_path.into(), contents.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lua.context::<_, anyhow::Result<()>>(|lua_ctx| {
|
||||
lua_ctx.add_searcher(map)?;
|
||||
let globals = lua_ctx.globals();
|
||||
|
||||
let lua_script_entry = std::fs::read_to_string(
|
||||
PathBuf::new()
|
||||
.join(&self.path)
|
||||
.join("scripts")
|
||||
.join(format!("{}.lua", &self.name)),
|
||||
)
|
||||
.context("failed to find lua script")?;
|
||||
|
||||
lua_ctx
|
||||
.load(&lua_script_entry)
|
||||
.set_name(&self.name)?
|
||||
.exec()?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
cuddle/src/actions/shell.rs
Normal file
62
cuddle/src/actions/shell.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::Stdio;
|
||||
use std::{env::current_dir, path::PathBuf, process::Command};
|
||||
|
||||
use crate::model::CuddleVariable;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ShellAction {
|
||||
path: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl ShellAction {
|
||||
pub fn new(name: String, path: String) -> Self {
|
||||
Self { path, name }
|
||||
}
|
||||
|
||||
pub fn execute(self, variables: Vec<CuddleVariable>) -> anyhow::Result<()> {
|
||||
log::debug!("executing shell action: {}", self.path.clone());
|
||||
|
||||
log::info!("Starting running shell action: {}", self.path.clone());
|
||||
|
||||
let path = PathBuf::from(self.path.clone());
|
||||
if !path.exists() {
|
||||
log::info!("script='{}' not found, aborting", path.to_string_lossy());
|
||||
return Err(anyhow::anyhow!("file not found aborting"));
|
||||
}
|
||||
|
||||
let current_dir = current_dir()?;
|
||||
log::trace!("current executable dir={}", current_dir.to_string_lossy());
|
||||
|
||||
let mut process = Command::new(&path)
|
||||
.current_dir(current_dir)
|
||||
.envs(variables.iter().map(|v| {
|
||||
log::trace!("{:?}", v);
|
||||
|
||||
(v.name.to_uppercase(), v.value.clone())
|
||||
}))
|
||||
.spawn()?;
|
||||
|
||||
let status = process.wait()?;
|
||||
|
||||
match status.code() {
|
||||
None => {
|
||||
log::warn!("process exited without code")
|
||||
}
|
||||
Some(n) => {
|
||||
if n > 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{} exited with: {}",
|
||||
path.clone().to_string_lossy(),
|
||||
n
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Finished running shell action");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
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")),
|
||||
}
|
||||
}
|
28
cuddle/src/config.rs
Normal file
28
cuddle/src/config.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use envconfig::Envconfig;
|
||||
|
||||
pub enum CuddleFetchPolicy {
|
||||
Always,
|
||||
Once,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Envconfig, Clone, Debug)]
|
||||
pub struct CuddleConfig {
|
||||
#[envconfig(from = "CUDDLE_FETCH_POLICY", default = "once")]
|
||||
fetch_policy: String,
|
||||
}
|
||||
|
||||
impl CuddleConfig {
|
||||
pub fn from_env() -> anyhow::Result<Self> {
|
||||
CuddleConfig::init_from_env().map_err(|e| anyhow::Error::from(e))
|
||||
}
|
||||
|
||||
pub fn get_fetch_policy(&self) -> anyhow::Result<CuddleFetchPolicy> {
|
||||
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")),
|
||||
}
|
||||
}
|
||||
}
|
215
cuddle/src/context.rs
Normal file
215
cuddle/src/context.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use std::{
|
||||
env::{self, current_dir},
|
||||
ffi::OsStr,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use git2::{
|
||||
build::{CheckoutBuilder, RepoBuilder},
|
||||
FetchOptions, RemoteCallbacks,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{CuddleConfig, CuddleFetchPolicy},
|
||||
model::{CuddleBase, CuddlePlan},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CuddleTreeType {
|
||||
Root,
|
||||
Leaf,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CuddleContext {
|
||||
pub plan: CuddlePlan,
|
||||
pub path: PathBuf,
|
||||
pub node_type: CuddleTreeType,
|
||||
}
|
||||
|
||||
pub fn extract_cuddle(
|
||||
config: CuddleConfig,
|
||||
) -> anyhow::Result<Option<Arc<Mutex<Vec<CuddleContext>>>>> {
|
||||
let mut curr_dir = current_dir()?;
|
||||
curr_dir.push(".cuddle/");
|
||||
let fetch_policy = config.get_fetch_policy()?;
|
||||
if let CuddleFetchPolicy::Always = fetch_policy {
|
||||
if curr_dir.exists() {
|
||||
if let Err(res) = std::fs::remove_dir_all(curr_dir) {
|
||||
panic!("{}", res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load main cuddle file.
|
||||
let cuddle_yaml = find_root_cuddle()?;
|
||||
if let None = cuddle_yaml {
|
||||
return Ok(None);
|
||||
}
|
||||
let cuddle_yaml = cuddle_yaml.unwrap();
|
||||
log::trace!(cuddle_yaml=log::as_debug!(cuddle_yaml); "Find root cuddle");
|
||||
|
||||
let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(cuddle_yaml.as_str())?;
|
||||
log::debug!(cuddle_plan=log::as_debug!(cuddle_yaml); "parse cuddle plan");
|
||||
|
||||
let context: Arc<Mutex<Vec<CuddleContext>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
context.lock().unwrap().push(CuddleContext {
|
||||
plan: cuddle_plan.clone(),
|
||||
path: current_dir()?,
|
||||
node_type: CuddleTreeType::Root,
|
||||
});
|
||||
|
||||
// pull parent plan and execute recursive descent
|
||||
match cuddle_plan.base {
|
||||
CuddleBase::Bool(true) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"plan cannot be enabled without specifying a plan"
|
||||
))
|
||||
}
|
||||
CuddleBase::Bool(false) => {
|
||||
log::debug!("plan is root: skipping");
|
||||
}
|
||||
CuddleBase::String(parent_plan) => {
|
||||
let destination_path = create_cuddle_local()?;
|
||||
let mut cuddle_dest = destination_path.clone();
|
||||
cuddle_dest.push("base");
|
||||
|
||||
if !cuddle_dest.exists() {
|
||||
pull_parent_cuddle_into_local(parent_plan, cuddle_dest.clone())?;
|
||||
}
|
||||
|
||||
recurse_parent(cuddle_dest, context.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(context))
|
||||
}
|
||||
|
||||
fn create_cuddle_local() -> anyhow::Result<PathBuf> {
|
||||
let mut curr_dir = current_dir()?.clone();
|
||||
curr_dir.push(".cuddle/");
|
||||
|
||||
if curr_dir.exists() {
|
||||
log::debug!(".cuddle/ already exists: skipping");
|
||||
return Ok(curr_dir);
|
||||
}
|
||||
|
||||
std::fs::create_dir(curr_dir.clone())?;
|
||||
|
||||
Ok(curr_dir)
|
||||
}
|
||||
|
||||
fn create_cuddle(path: PathBuf) -> anyhow::Result<PathBuf> {
|
||||
let mut curr_dir = path.clone();
|
||||
curr_dir.push(".cuddle/");
|
||||
|
||||
if curr_dir.exists() {
|
||||
log::debug!(".cuddle/ already exists: skipping");
|
||||
return Ok(curr_dir);
|
||||
}
|
||||
|
||||
std::fs::create_dir(curr_dir.clone())?;
|
||||
|
||||
Ok(curr_dir)
|
||||
}
|
||||
|
||||
fn pull_parent_cuddle_into_local(
|
||||
parent_cuddle: String,
|
||||
destination: PathBuf,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut rc = RemoteCallbacks::new();
|
||||
rc.credentials(|_url, username_from_url, _allowed_types| {
|
||||
if "true".to_string() == std::env::var("CUDDLE_SSH_AGENT").ok().unwrap_or("".into()) {
|
||||
git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
|
||||
} else {
|
||||
git2::Cred::ssh_key(
|
||||
username_from_url.unwrap(),
|
||||
None,
|
||||
Path::new(&format!("{}/.ssh/id_ed25519", env::var("HOME").unwrap())),
|
||||
None,
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
rc.certificate_check(|_cert, _something| Ok(git2::CertificateCheckStatus::CertificateOk));
|
||||
|
||||
let mut fo = FetchOptions::new();
|
||||
fo.remote_callbacks(rc);
|
||||
|
||||
let co = CheckoutBuilder::new();
|
||||
RepoBuilder::new()
|
||||
.fetch_options(fo)
|
||||
.with_checkout(co)
|
||||
.clone(&parent_cuddle, &destination)?;
|
||||
|
||||
log::debug!(parent_cuddle=log::as_display!(parent_cuddle); "pulled repository");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recurse_parent(
|
||||
path: PathBuf,
|
||||
context: Arc<Mutex<Vec<CuddleContext>>>,
|
||||
) -> anyhow::Result<Option<()>> {
|
||||
let cuddle_contents = find_cuddle(path.clone())?;
|
||||
if let None = cuddle_contents {
|
||||
return Ok(None);
|
||||
}
|
||||
let cuddle_plan = serde_yaml::from_str::<CuddlePlan>(&cuddle_contents.unwrap())?;
|
||||
|
||||
let ctx = context.clone();
|
||||
if let Ok(mut ctxs) = ctx.lock() {
|
||||
ctxs.push(CuddleContext {
|
||||
plan: cuddle_plan.clone(),
|
||||
path: path.clone(),
|
||||
node_type: CuddleTreeType::Leaf,
|
||||
});
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Could not acquire lock, aborting"));
|
||||
}
|
||||
|
||||
match cuddle_plan.base {
|
||||
CuddleBase::Bool(true) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"plan cannot be enabled without specifying a plan"
|
||||
))
|
||||
}
|
||||
CuddleBase::Bool(false) => {
|
||||
log::debug!("plan is root: finishing up");
|
||||
return Ok(Some(()));
|
||||
}
|
||||
CuddleBase::String(parent_plan) => {
|
||||
let destination_path = create_cuddle(path.clone())?;
|
||||
let mut cuddle_dest = destination_path.clone();
|
||||
cuddle_dest.push("base");
|
||||
|
||||
if !cuddle_dest.exists() {
|
||||
pull_parent_cuddle_into_local(parent_plan, cuddle_dest.clone())?;
|
||||
}
|
||||
return recurse_parent(cuddle_dest, context.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_root_cuddle() -> anyhow::Result<Option<String>> {
|
||||
// TODO: Make recursive towards root
|
||||
let current_dir = env::current_dir()?;
|
||||
find_cuddle(current_dir)
|
||||
}
|
||||
|
||||
fn find_cuddle(path: PathBuf) -> anyhow::Result<Option<String>> {
|
||||
for entry in std::fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
let metadata = std::fs::metadata(&path)?;
|
||||
if metadata.is_file() && path.file_name().unwrap() == OsStr::new("cuddle.yaml") {
|
||||
return Ok(Some(std::fs::read_to_string(path)?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
31
cuddle/src/main.rs
Normal file
31
cuddle/src/main.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use config::CuddleConfig;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
mod actions;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod context;
|
||||
mod model;
|
||||
mod util;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
init_logging()?;
|
||||
let _ = dotenv::dotenv();
|
||||
|
||||
let config = CuddleConfig::from_env()?;
|
||||
|
||||
let context = context::extract_cuddle(config.clone())?;
|
||||
_ = cli::CuddleCli::new(context, config)?.execute()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_logging() -> anyhow::Result<()> {
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer())
|
||||
.with(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
Ok(())
|
||||
}
|
79
cuddle/src/model.rs
Normal file
79
cuddle/src/model.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CuddleBase {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct CuddleShellScriptArgEnv {
|
||||
pub key: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct CuddleShellScriptArgFlag {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub required: Option<bool>,
|
||||
pub default_value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum CuddleShellScriptArg {
|
||||
#[serde(alias = "env")]
|
||||
Env(CuddleShellScriptArgEnv),
|
||||
#[serde(alias = "flag")]
|
||||
Flag(CuddleShellScriptArgFlag),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct CuddleShellScript {
|
||||
pub description: Option<String>,
|
||||
pub args: Option<HashMap<String, CuddleShellScriptArg>>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct CuddleDaggerScript {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct CuddleLuaScript {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum CuddleScript {
|
||||
#[serde(alias = "shell")]
|
||||
Shell(CuddleShellScript),
|
||||
#[serde(alias = "dagger")]
|
||||
Dagger(CuddleDaggerScript),
|
||||
#[serde(alias = "lua")]
|
||||
Lua(CuddleLuaScript),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct CuddlePlan {
|
||||
pub base: CuddleBase,
|
||||
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 }
|
||||
}
|
||||
}
|
33
cuddle/src/util/git.rs
Normal file
33
cuddle/src/util/git.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
1
cuddle/src/util/mod.rs
Normal file
1
cuddle/src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod git;
|
Reference in New Issue
Block a user