use crate::{
model::{
cluster::{Cluster, NodeEntry, RootEntry},
config_dir, data_dir,
hook::{HookAction, HookKind, HookRunner},
},
store::{DeployAction, MultiNodeClone, Node, Root, TablizeCluster},
};
use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use inquire::prompt_confirmation;
use std::{ffi::OsString, fs::remove_dir_all};
use tracing::{info, instrument, warn};
#[derive(Debug, Clone, Parser)]
#[command(
about,
override_usage = "\n ocd [options] <ocd-command>\n ocd [options] [target]... <git-command>",
subcommand_help_heading = "Commands",
version
)]
pub struct Ocd {
#[arg(default_value_t = HookAction::default(), long, short, value_enum, value_name = "action")]
pub run_hook: HookAction,
#[command(subcommand)]
pub command: Command,
}
impl Ocd {
pub async fn run(self) -> Result<()> {
match self.command {
Command::Clone(opts) => run_clone(self.run_hook, opts).await,
Command::Init(opts) => run_init(self.run_hook, opts),
Command::Deploy(opts) => run_deploy(self.run_hook, opts),
Command::Undeploy(opts) => run_undeploy(self.run_hook, opts),
Command::Remove(opts) => run_remove(self.run_hook, opts),
Command::List(opts) => run_list(self.run_hook, opts),
Command::Git(opts) => run_git(opts),
}
}
}
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
#[command(override_usage = "ocd clone [options] <url>")]
Clone(CloneOptions),
#[command(override_usage = "ocd init [options] <node_name>")]
Init(InitOptions),
#[command(override_usage = "ocd deploy [options] [target]...")]
Deploy(DeployOptions),
#[command(override_usage = "ocd undeploy [options] [target]...")]
Undeploy(UndeployOptions),
#[command(name = "rm", override_usage = "ocd rm [options] [target]...")]
Remove(RemoveOptions),
#[command(name = "ls", override_usage = "ocd list [options]")]
List(ListOptions),
#[command(external_subcommand)]
Git(Vec<OsString>),
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct CloneOptions {
#[arg(value_name = "url")]
pub url: String,
#[arg(short, long, value_name = "limit")]
pub jobs: Option<usize>,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct InitOptions {
#[arg(value_name = "entry_name")]
pub entry_name: String,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct DeployOptions {
#[arg(value_parser, num_args = 1.., value_delimiter = ',', value_name = "pattern")]
pub patterns: Vec<String>,
#[arg(short, long)]
pub only: bool,
#[arg(short, long)]
pub with_excluded: bool,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct UndeployOptions {
#[arg(value_parser, num_args = 1.., value_delimiter = ',', value_name = "pattern")]
pub patterns: Vec<String>,
#[arg(short, long)]
pub only: bool,
#[arg(short, long)]
pub excluded_only: bool,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct RemoveOptions {
#[arg(value_parser, num_args = 1.., value_delimiter = ',', value_name = "pattern")]
pub patterns: Vec<String>,
}
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct ListOptions {
#[arg(short, long)]
pub names_only: bool,
}
#[instrument(skip(opts), level = "debug")]
async fn run_clone(action: HookAction, opts: CloneOptions) -> Result<()> {
if let Err(error) = Root::new_clone(&opts.url) {
warn!("Root clone failure, clearing broken cluster");
let config_dir = config_dir()?;
if config_dir.exists() {
remove_dir_all(&config_dir)
.with_context(|| format!("Failed to remove {config_dir:?}"))?;
}
let data_dir = data_dir()?;
if data_dir.exists() {
remove_dir_all(&data_dir).with_context(|| format!("Failed to remove {data_dir:?}"))?;
}
return Err(error);
}
let cluster = Cluster::new()?;
let mut hooks = HookRunner::new()?;
hooks.set_action(action);
hooks.run("clone", HookKind::Pre, None)?;
let multi_clone = MultiNodeClone::new(&cluster, opts.jobs)?;
multi_clone.clone_all().await?;
hooks.run("clone", HookKind::Post, None)?;
Ok(())
}
pub fn run_init(action: HookAction, opts: InitOptions) -> Result<()> {
let mut hooks = HookRunner::new()?;
hooks.set_action(action);
hooks.run("init", HookKind::Pre, Some(&vec![opts.entry_name.clone()]))?;
match opts.entry_name.as_str() {
"root" => {
let path = config_dir()?.join(format!("{}.toml", opts.entry_name));
if !path.exists() {
return Err(anyhow!("No root entry to initialize! Define {path:?} first!"));
}
let data = std::fs::read_to_string(path)?;
let root: RootEntry = toml::de::from_str(&data)?;
let _ = Root::new_init(&root)?;
}
&_ => {
let cluster = Cluster::new()?;
let _ = Root::new_open(&cluster.root)
.with_context(|| "Root may not have been properly initialized")?;
let path = config_dir()?.join("nodes").join(format!("{}.toml", opts.entry_name));
if !path.exists() {
return Err(anyhow!("No node entry to initialize! Define {path:?} first!"));
}
let data = std::fs::read_to_string(path)?;
let node: NodeEntry = toml::de::from_str(&data)?;
let _ = Node::new_init(&opts.entry_name, &node)?;
}
}
hooks.run("init", HookKind::Post, Some(&vec![opts.entry_name.clone()]))?;
Ok(())
}
#[instrument(skip(opts), level = "debug")]
pub fn run_deploy(run_hook: HookAction, opts: DeployOptions) -> Result<()> {
let cluster = Cluster::new()?;
let root = Root::new_open(&cluster.root)?;
let action = if opts.with_excluded { DeployAction::DeployAll } else { DeployAction::Deploy };
let targets = cluster.match_targets(opts.patterns)?;
let mut hooks = HookRunner::new()?;
hooks.set_action(run_hook);
hooks.run("deploy", HookKind::Pre, Some(&targets))?;
let mut nodes = Vec::new();
if opts.only {
for target in &targets {
if target == "root" {
root.deploy(action)?;
continue;
}
let entry = cluster.nodes.get(target).ok_or(anyhow!("Node {target:?} not defined"))?;
let node = Node::new_open(target, entry)?;
nodes.push(node);
}
} else {
for target in &targets {
if target == "root" {
root.deploy(action)?;
continue;
}
for (name, entry) in cluster.dependency_iter(target) {
let node = Node::new_open(name, entry)?;
nodes.push(node);
}
}
}
for node in nodes {
node.deploy(action)?;
}
hooks.run("deploy", HookKind::Post, Some(&targets))?;
Ok(())
}
fn run_undeploy(run_hook: HookAction, opts: UndeployOptions) -> Result<()> {
let cluster = Cluster::new()?;
let root = Root::new_open(&cluster.root)?;
let action =
if opts.excluded_only { DeployAction::UndeployExcludes } else { DeployAction::Undeploy };
let targets = cluster.match_targets(opts.patterns)?;
let mut hooks = HookRunner::new()?;
hooks.set_action(run_hook);
hooks.run("undeploy", HookKind::Pre, Some(&targets))?;
let mut nodes = Vec::new();
if opts.only {
for target in &targets {
if target == "root" {
root.deploy(action)?;
continue;
}
let entry = cluster.nodes.get(target).ok_or(anyhow!("Node {target:?} not defined"))?;
let node = Node::new_open(target, entry)?;
nodes.push(node);
}
} else {
for target in &targets {
if target == "root" {
root.deploy(action)?;
continue;
}
for (name, entry) in cluster.dependency_iter(target) {
let node = Node::new_open(name, entry)?;
nodes.push(node);
}
}
}
for node in nodes {
node.deploy(action)?;
}
hooks.run("undeploy", HookKind::Post, Some(&targets))?;
Ok(())
}
#[instrument(skip(opts), level = "debug")]
fn run_remove(run_hook: HookAction, opts: RemoveOptions) -> Result<()> {
let cluster = Cluster::new()?;
let targets = cluster.match_targets(opts.patterns)?;
let mut hooks = HookRunner::new()?;
hooks.set_action(run_hook);
hooks.run("rm", HookKind::Pre, Some(&targets))?;
if targets.contains(&"root".into()) {
warn!("Removing root will nuke your entire cluster");
if prompt_confirmation("Do you want to send your cluster to the gallows? [y/n]")? {
nuke_cluster(&cluster)?;
}
} else {
for target in &targets {
let node = cluster.nodes.get(target).ok_or(anyhow!("Node {target:?} not defined"))?;
let repo = Node::new_open(target, node)?;
repo.nuke()?;
}
}
hooks.run("rm", HookKind::Post, Some(&targets))?;
Ok(())
}
fn nuke_cluster(cluster: &Cluster) -> Result<()> {
let root = Root::new_open(&cluster.root)?;
root.nuke()?;
for (name, node) in &cluster.nodes {
if !data_dir()?.join(name).exists() {
warn!("Node {name:?} not found in repository store");
continue;
}
let repo = Node::new_open(name, node)?;
repo.nuke()?;
}
remove_dir_all(config_dir()?)?;
info!("Configuration directory removed");
remove_dir_all(data_dir()?)?;
info!("Data directory removed");
Ok(())
}
fn run_list(run_hook: HookAction, opts: ListOptions) -> Result<()> {
let cluster = Cluster::new()?;
let root = Root::new_open(&cluster.root)?;
let mut hooks = HookRunner::new()?;
hooks.set_action(run_hook);
hooks.run("ls", HookKind::Pre, None)?;
let tablize = TablizeCluster::new(&root, &cluster);
if opts.names_only {
tablize.names_only()?;
} else {
tablize.fancy()?;
}
hooks.run("ls", HookKind::Post, None)?;
Ok(())
}
fn run_git(opts: Vec<OsString>) -> Result<()> {
let cluster = Cluster::new()?;
let root = Root::new_open(&cluster.root)?;
let patterns = opts[0].to_string_lossy().into_owned();
let patterns: Vec<String> = patterns.split(',').map(Into::into).collect();
let targets = cluster.match_targets(patterns)?;
for target in &targets {
if target == "root" {
root.gitcall(opts[1..].to_vec())?;
continue;
}
let node = cluster.nodes.get(target).ok_or(anyhow!("{target} not found"))?;
let node = Node::new_open(target, node)?;
node.gitcall(opts[1..].to_vec())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_verify_structure() {
Ocd::command().debug_assert();
}
}