feat: refactor, accept multiple setup/cleanup ...

run setup and cleanup inside git directory but tci script in tempdir,
better handle errors, no longer receive config from argv, also very much
refactor
This commit is contained in:
əlemi 2024-02-13 20:59:38 +01:00
parent 57683aab78
commit 4a1fef20fd
Signed by: alemi
GPG key ID: A4895B84D311642C

View file

@ -1,15 +1,20 @@
use std::path::PathBuf;
use git2::{Config, Repository};
#[derive(Debug, thiserror::Error)]
enum TciError {
#[error("could not understand file system structure, bailing out")]
FsError,
FsError(&'static str),
#[error("missing tci script")]
MissingScript,
#[error("there is no argv0 (wtf??), can't figure out which hook invoked tci!")]
MissingArgv0,
#[error("unexpected i/o error")]
IoError(#[from] std::io::Error),
#[error("git error")]
GitError(#[from] git2::Error),
}
#[derive(Debug, Default, serde::Deserialize)]
@ -18,24 +23,18 @@ struct TciConfig {
allow_all: bool,
#[serde(default)]
setup: Option<String>,
setup: Vec<String>,
#[serde(default)]
cleanup: Option<String>,
cleanup: Vec<String>,
}
fn main() {
if let Err(e) = tci() {
println!("[!] error running tci: {e}");
}
}
fn tci() -> Result<(), Box<dyn std::error::Error>> {
// load tci config
let config_path = std::env::args().nth(1)
.as_deref()
.unwrap_or("/etc/tci/config.toml")
.to_string();
// TODO how could we receive this?
// arguments are used by git
// idk how we could reliably set env vars???
let config_path = "/etc/tci/config.toml";
let cfg_raw = std::fs::read_to_string(config_path)
.unwrap_or_default();
@ -46,61 +45,9 @@ fn tci() -> Result<(), Box<dyn std::error::Error>> {
TciConfig::default()
});
// find out which hook is invoking tci
let callback = std::path::Path::new(&std::env::args().next().ok_or(TciError::MissingArgv0)?)
.file_name().ok_or(TciError::FsError)?
.to_str().ok_or(TciError::FsError)?
.to_string();
let repo_path = std::env::current_dir()?;
let repo_name = repo_path
.parent().ok_or(TciError::FsError)?
.file_name().ok_or(TciError::FsError)?
.to_str().ok_or(TciError::FsError)?
.to_string();
// check config before even creating temp dir and cloning repo
let git_config = Config::open(&repo_path.join("config"))?;
if !cfg.allow_all && !git_config.get_bool("tci.allow").unwrap_or(false) {
return Ok(()); // we are in whitelist mode and this repo is not whitelisted
if let Err(e) = tci(cfg) {
println!("[!] error running tci: {e}");
}
let tmp = tempdir::TempDir::new(&format!("tci-{repo_name}"))?;
Repository::clone(repo_path.to_str().ok_or(TciError::FsError)?, tmp.path())?;
std::env::set_current_dir(tmp.path())?;
if let Some(setup) = cfg.setup {
print!("[+] setting up ('{setup}')");
let res = shell(setup)?;
if !res.is_empty() {
print!(": {res}");
}
println!();
}
println!("[o] running tci hook [{callback}] for repo '{repo_name}'");
let tci_script = git_config.get_str("tci.script").unwrap_or(".tci").to_string();
if !tmp.path().join(&tci_script).is_file() {
return Err(Box::new(TciError::MissingScript));
}
let res = shell(tmp.path().join(&tci_script))?;
println!("{res}");
if let Some(cleanup) = cfg.cleanup {
print!("[-] cleaning up ('{cleanup}')");
let res = shell(cleanup)?;
if !res.is_empty() {
print!(": {res}");
}
println!();
}
Ok(())
}
fn shell<S : AsRef<std::ffi::OsStr>>(cmd: S) -> std::io::Result<String> {
@ -109,8 +56,79 @@ fn shell<S : AsRef<std::ffi::OsStr>>(cmd: S) -> std::io::Result<String> {
match std::str::from_utf8(&output.stdout) {
Ok(s) => Ok(s.to_string()),
Err(e) => {
println!("[?] script produced non-utf8 output ({e}), showing as bytes");
println!("[?] shell produced non-utf8 output ({e})");
Ok(format!("{:?}", output.stdout))
},
}
}
fn tci(cfg: TciConfig) -> Result<(), Box<dyn std::error::Error>> {
let repo_path = std::env::current_dir()?;
// check config before even creating temp dir and cloning repo
let git_config = Config::open(&repo_path.join("config"))?;
let tci_script = git_config.get_str("tci.script").unwrap_or(".tci").to_string();
// check if CI is allowed
if !cfg.allow_all && !git_config.get_bool("tci.allow").unwrap_or(false) {
return Ok(()); // we are in whitelist mode and this repo is not whitelisted
}
// run setup hooks, if provided
for setup in cfg.setup {
print!("[+] setting up ('{setup}')");
let res = shell(setup)?;
if !res.is_empty() {
print!(": {res}");
}
println!();
}
let res = tci_hook(&repo_path, &tci_script);
for cleanup in cfg.cleanup {
print!("[-] cleaning up ('{cleanup}')");
let res = shell(cleanup)?;
if !res.is_empty() {
print!(": {res}");
}
println!();
}
res?; // only check error AFTER running cleanup hooks
println!("[o] script run successfully");
Ok(())
}
fn tci_hook(repo_path: &PathBuf, tci_script: &str) -> Result<(), TciError> {
let repo_name = repo_path
.parent().ok_or(TciError::FsError("missing .git parent"))?
.file_name().ok_or(TciError::FsError("no file name for .git parent"))?
.to_str().ok_or(TciError::FsError(".git parent file name is not a valid string"))?
.to_string();
let tmp = tempdir::TempDir::new(&format!("tci-{repo_name}"))?;
// TODO allow customizing clone? just clone recursive? just let hook setup submodules?
Repository::clone(
repo_path.to_str()
.ok_or(TciError::FsError("repo path is not a valid string"))?,
tmp.path(),
)?;
if !tmp.path().join(tci_script).is_file() {
return Err(TciError::MissingScript);
}
std::env::set_current_dir(tmp.path())?;
println!("[o] running tci script for repo '{repo_name}'");
let res = shell(tmp.path().join(tci_script))?;
println!("{res}");
std::env::set_current_dir(repo_path)?;
Ok(())
}