use anyhow::{anyhow, bail};
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub mod observe;
pub mod patch;
pub mod staged;
pub mod storage_add;
pub mod storage_use;
pub mod test;
use crate::actions::staged::{Stage, StagedAction};
use crate::actions::{
observe::ObserveAction, patch::PatchAction, storage_add::AddToStorageAction, storage_use::UseFromStorageAction,
test::TestAction,
};
use crate::bset;
use crate::cmd::{CatActionArgs, EditActionArgs, NewActionArgs, RemoveActionArgs};
use crate::entities::info::StrToInfo;
use crate::entities::variables::Variable;
use crate::entities::{
custom_command::CustomCommand,
environment::RunEnvironment,
info::{ActionInfo, ShortName, info2str, str2info},
requirements::Requirement,
};
use crate::globals::DeployerGlobalConfig;
use crate::pipelines::DescribedPipeline;
use crate::project::DeployerProjectOptions;
use crate::rw::read_checked;
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct UsedAction {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub used: ActionInfo,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub with: BTreeMap<String, ShortName>,
}
impl UsedAction {
pub fn definition<'project>(
&'project self,
definitions: &'project BTreeSet<DefinedAction>,
) -> anyhow::Result<&'project DefinedAction> {
definitions
.iter()
.find(|d| d.info.eq(&self.used))
.ok_or(anyhow::anyhow!(
"Can't find definition of `{}` action!",
self.used.to_str()
))
}
pub fn commands<'project>(
&'project self,
definitions: &'project BTreeSet<DefinedAction>,
) -> anyhow::Result<Vec<&'project CustomCommand>> {
self.definition(definitions)?.commands()
}
}
#[derive(Deserialize, Serialize, Eq, Clone)]
pub struct DefinedAction {
#[serde(serialize_with = "info2str", deserialize_with = "str2info")]
pub info: ActionInfo,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requirements: Vec<Requirement>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exec_in_project_dir: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_sync: Option<bool>,
pub action: Action,
}
impl PartialEq for DefinedAction {
fn eq(&self, other: &Self) -> bool {
self.info.eq(&other.info)
}
}
impl Ord for DefinedAction {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.info.cmp(&other.info)
}
}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for DefinedAction {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.info.cmp(&other.info))
}
}
impl DefinedAction {
pub fn commands(&self) -> anyhow::Result<Vec<&CustomCommand>> {
let cmds = match &self.action {
Action::Interrupt
| Action::SyncToRemote { .. }
| Action::SyncFromRemote { .. }
| Action::AddToStorage(_)
| Action::UseFromStorage(_)
| Action::Patch(_) => vec![],
Action::Custom(cmd) => vec![cmd],
Action::Staged(a) => vec![&a.command],
Action::Test(ta) => vec![&ta.command],
Action::Observe(oa) => vec![&oa.command],
};
Ok(cmds)
}
pub fn collect_required_variables(&self) -> BTreeSet<String> {
let mut vars = bset![];
match &self.action {
Action::Custom(cmd) => {
cmd.placeholders.iter().for_each(|item| {
vars.insert(item.to_owned());
});
}
Action::Staged(a) => {
a.command.placeholders.iter().for_each(|item| {
vars.insert(item.to_owned());
});
}
Action::Test(ta) => {
ta.command.placeholders.iter().for_each(|item| {
vars.insert(item.to_owned());
});
}
Action::Observe(oa) => {
oa.command.placeholders.iter().for_each(|item| {
vars.insert(item.to_owned());
});
}
_ => {}
}
vars
}
pub async fn to_shell(
&self,
env: &RunEnvironment<'_>,
vars: &BTreeMap<String, Variable>,
) -> anyhow::Result<Vec<String>> {
match &self.action {
Action::Interrupt if !env.containered_build => Ok(vec!["read -p \"Press Enter to continue...\"".to_string()]),
Action::Custom(c) => c.to_shell(env, vars).await,
Action::Test(c) => c.to_shell(env, vars).await,
Action::Staged(a) if a.stage() != Stage::Deploy || !env.containered_build => a.to_shell(env, vars).await,
Action::Observe(c) if !env.containered_build => c.to_shell(env, vars).await,
Action::Patch(c) => c.to_shell(env).await,
Action::AddToStorage(c) => c.to_shell(env).await,
Action::UseFromStorage(c) if !env.containered_build && !env.containered_run => c.to_shell(env).await,
_ => {
println!("Skip action during translation into shell script...");
Ok(vec![])
}
}
}
pub fn r#use(&self) -> UsedAction {
UsedAction {
title: None,
used: self.info.clone(),
with: Default::default(),
}
}
pub fn is_always_piped(&self) -> bool {
match &self.action {
Action::Custom(c) => c.show_success_output.is_some_and(|v| v),
Action::Staged(s) => s.command.show_success_output.is_some_and(|v| v),
Action::Test(t) => t.command.show_success_output.is_some_and(|v| v),
Action::Observe(_) => true,
_ => false,
}
}
pub fn is_observer(&self) -> bool {
matches!(&self.action, Action::Observe(_))
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum Action {
Interrupt,
SyncToRemote {
remote_host_name: ShortName,
},
SyncFromRemote {
remote_host_name: ShortName,
},
Custom(CustomCommand),
Staged(StagedAction),
Test(TestAction),
Observe(ObserveAction),
UseFromStorage(UseFromStorageAction),
AddToStorage(AddToStorageAction),
Patch(PatchAction),
}
impl Action {
pub fn interrupt() -> anyhow::Result<(bool, Vec<String>)> {
println!();
inquire_reorder::Confirm::new("The pipeline is interrupted. Hit `Enter` to continue")
.with_default(true)
.prompt()?;
let _ = std::io::read_to_string(std::io::stdin())?;
Ok((true, vec![]))
}
pub async fn run_with(
&self,
config: &DeployerProjectOptions,
pipeline: &DescribedPipeline,
env: &mut RunEnvironment<'_>,
with: &BTreeMap<String, ShortName>,
) -> anyhow::Result<(bool, Vec<String>)> {
use crate::pipelines::place_artifacts;
use crate::remote::{sync_from_remote, sync_to_remote};
let vars = config.variables_for(with)?;
let (status, output) = match self {
Action::SyncToRemote { remote_host_name } => {
if let Some(remote) = env.remotes.get(remote_host_name) {
use std::{collections::HashSet, path::PathBuf};
let mut ignore = HashSet::new();
config.cache_files.iter().for_each(|i| {
ignore.insert(i.to_owned());
});
config.ignore_files.iter().for_each(|i| {
ignore.insert(i.to_owned());
});
ignore.insert(PathBuf::from("artifacts"));
if let Err(e) = sync_to_remote(env.run_dir, remote, &ignore) {
(false, vec![e.to_string()])
} else {
(true, vec![])
}
} else {
(
false,
vec!["There is no such remote host in Deployer's Registry!".to_string()],
)
}
}
Action::SyncFromRemote { remote_host_name } => {
if let Some(remote) = env.remotes.get(remote_host_name) {
if let Err(e) = sync_from_remote(env.run_dir, remote) {
(false, vec![e.to_string()])
} else {
(true, vec![])
}
} else {
(
false,
vec!["There is no such remote host in Deployer's Registry!".to_string()],
)
}
}
Action::Custom(cmd) => cmd.execute(env, &vars).await?,
Action::Test(test) => test.execute(env, &vars).await?,
Action::Staged(a) if a.stage() != Stage::Deploy || !env.containered_build => a.execute(env, &vars).await?,
Action::Observe(o_action) if !env.containered_build => o_action.execute(env, &vars).await?,
Action::Patch(patch) => patch.execute(env).await?,
Action::Interrupt if !env.containered_build => Action::interrupt()?,
Action::UseFromStorage(use_from_action) if !env.containered_build && !env.containered_run => {
use_from_action.execute(env).await?
}
Action::AddToStorage(rules) if !env.containered_build && !env.containered_run => {
let placements = pipeline.collect_artifacts_placements(config, env).await?;
place_artifacts(env, placements, false)?;
rules.execute(env).await?
}
_ => (
true,
vec!["Skip this action due to not supporting when running in containers...".to_string()],
),
};
Ok((status, output))
}
}
pub fn list_actions(globals: &DeployerGlobalConfig) {
println!("Available actions in Deployer's Registry:");
for action in globals.actions_registry.iter() {
let action_info = action.info.to_str();
let tags = if action.tags.is_empty() {
String::new()
} else {
format!(" (tags: {})", action.tags.join(", ").as_str().blue().italic())
};
println!("• {} {}", action_info.blue().bold(), tags);
}
}
fn choose_action<'a>(actions_registry: &'a BTreeSet<DefinedAction>, prompt: &str) -> anyhow::Result<&'a DefinedAction> {
if actions_registry.is_empty() {
bail!("There is no actions in the Registry.");
}
let keys = actions_registry
.iter()
.map(|action| action.info.to_str())
.collect::<Vec<_>>();
let selected = inquire_reorder::Select::new(prompt, keys).prompt()?;
actions_registry
.iter()
.find(|action| action.info.to_str().eq(&selected))
.ok_or(anyhow!("No such action!"))
}
pub fn remove_action(globals: &mut DeployerGlobalConfig, args: RemoveActionArgs) -> anyhow::Result<()> {
let action = if let Some(info) = args.info {
let info = info.to_info()?;
globals
.actions_registry
.iter()
.find(|action| action.info.eq(&info))
.ok_or(anyhow!("This action is not found in registry!"))?
.clone()
} else {
choose_action(
&globals.actions_registry,
"Select action for removing from the registry:",
)?
.clone()
};
if !args.yes && !inquire_reorder::Confirm::new("Are you sure? (y/n)").prompt()? {
return Ok(());
}
globals.actions_registry.remove(&action);
Ok(())
}
pub fn new_action(globals: &mut DeployerGlobalConfig, args: &NewActionArgs) -> anyhow::Result<DefinedAction> {
if let Some(from_file) = &args.from {
let action = read_checked::<DefinedAction>(from_file)
.map_err(|e| {
panic!("Can't read provided Action file due to: {e}");
})
.unwrap();
globals.actions_registry.insert(action.clone());
return Ok(action);
}
let defined_action = DefinedAction::new_from_prompt(Some(globals))?;
Ok(defined_action)
}
pub fn cat_action(globals: &DeployerGlobalConfig, args: CatActionArgs) -> anyhow::Result<()> {
let action = if let Some(info) = args.info {
let info = info.to_info()?;
globals
.actions_registry
.iter()
.find(|action| action.info.eq(&info))
.ok_or(anyhow!("This action is not found in registry!"))?
.clone()
} else {
choose_action(&globals.actions_registry, "Select action for displaying:")?.clone()
};
let action_ser = serde_pretty_yaml::to_string_pretty(&action)?;
println!("{action_ser}");
Ok(())
}
pub async fn edit_action(globals: &mut DeployerGlobalConfig, args: EditActionArgs) -> anyhow::Result<()> {
let mut action = if let Some(info) = args.info {
let info = info.to_info()?;
globals
.actions_registry
.iter()
.find(|action| action.info.eq(&info))
.ok_or(anyhow!("This action is not found in registry!"))?
.clone()
} else {
choose_action(&globals.actions_registry, "Select action for editing:")?.clone()
};
action.edit_action_from_prompt(None).await?;
globals
.actions_registry
.retain(|in_registry| in_registry.info.ne(&action.info));
globals.actions_registry.insert(action);
Ok(())
}