use anyhow::bail;
use colored::Colorize;
use regex::Regex;
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use crate::actions::staged::Stage;
use crate::actions::storage_use::UseFromStorageAction;
use crate::actions::{
Action, new_action, observe::ObserveAction, patch::PatchAction, staged::StagedAction,
storage_add::AddToStorageAction, test::TestAction,
};
use crate::actions::{DefinedAction, UsedAction};
use crate::cmd::NewActionArgs;
use crate::entities::ansible_opts::AnsibleOpts;
use crate::entities::auto_version::AutoVersionExtractFromRule;
use crate::entities::compose_opts::{ComposeOpts, ComposeServiceOpts};
use crate::entities::containered_opts::{ContaineredOpts, PortBinding};
use crate::entities::custom_command::CustomCommand;
use crate::entities::driver::PipelineDriver;
use crate::entities::info::{ActionInfo, ContentInfo, PipelineInfo, ShortName};
use crate::entities::placements::Placement;
use crate::entities::remote_host::RemoteHost;
use crate::entities::requirements::Requirement;
use crate::entities::variables::{FromEnvFile, Kv2Paths, VAULT_ADDR_ENV, VAULT_ADDR_TOKEN, VarValue, Variable};
use crate::globals::DeployerGlobalConfig;
use crate::pipelines::DescribedPipeline;
use crate::project::DeployerProjectOptions;
use crate::utils::tags_custom_type;
use crate::{bmap, bset};
impl DeployerProjectOptions {
pub fn init_from_prompt(&mut self, curr_dir: String) -> anyhow::Result<()> {
use inquire_reorder::Text;
#[cfg(unix)]
let curr_dir = curr_dir.split('/').next_back().unwrap();
let project_name_proposal = if self.project_name.is_empty() {
curr_dir.to_owned()
} else {
self.project_name.to_owned()
};
self.project_name = Text::new("Enter the project's name:")
.with_initial_value(project_name_proposal.as_str())
.prompt()?;
self.ignore_files.insert(PathBuf::from(".git"));
self.variables = collect_variables()?;
Ok(())
}
}
impl DefinedAction {
pub fn new_from_prompt(opts: Option<&mut DeployerGlobalConfig>) -> anyhow::Result<Self> {
use inquire_reorder::{Select, Text};
let info = Text::new("Specify the Action's short name and version:")
.with_placeholder("action-name@0.1.0")
.prompt()?;
let info = ActionInfo::try_from_str(&info)?;
let tags = vec![];
let action_types: Vec<&str> = vec![
"Staged (most common)",
"Custom",
"Patch",
"Test",
"Observe",
"Sync build folder to remote",
"Sync build artifacts from remote",
"Use content from storage",
"Add content to storage",
];
let selected_action_type =
Select::new("Select action's type (read the docs for details):", action_types).prompt()?;
let action = match selected_action_type {
"Custom" => {
let command = CustomCommand::new_from_prompt()?;
Action::Custom(command)
}
"Staged (most common)" => Action::Staged(StagedAction::new_from_prompt()?),
"Test" => Action::Test(TestAction::new_from_prompt()?),
"Observe" => Action::Observe(ObserveAction {
command: CustomCommand::new_observe_from_prompt()?,
}),
"Patch" => Action::Patch(PatchAction::new_from_prompt()?),
"Use content from storage" => {
let short_name = Text::new("Write the content's short name:").prompt()?;
let version = Text::new("Specify the content's version:").prompt()?;
let info = ContentInfo::new_for_using(short_name, version)?;
Action::UseFromStorage(UseFromStorageAction {
content_info: info,
subfolder: None,
})
}
"Add content to storage" => {
let short_name = Text::new("Write the content's short name:").prompt()?;
let auto_version_rule = AutoVersionExtractFromRule::new_from_prompt()?;
Action::AddToStorage(AddToStorageAction {
short_name,
auto_version_rule,
content: Default::default(),
})
}
"Sync build folder to remote" => Action::SyncToRemote {
remote_host_name: ShortName::new(
inquire_reorder::Text::new("Specify the remote host's short name:").prompt()?,
)?,
},
"Sync build artifacts from remote" => Action::SyncFromRemote {
remote_host_name: ShortName::new(
inquire_reorder::Text::new("Specify the remote host's short name:").prompt()?,
)?,
},
_ => unreachable!(),
};
let described_action = DefinedAction {
info,
tags,
action,
requirements: vec![],
exec_in_project_dir: None,
skip_sync: None,
};
if let Some(opts) = opts {
if opts
.actions_registry
.iter()
.any(|action| action.info == described_action.info)
&& !inquire_reorder::Confirm::new(&format!(
"Actions Registry already have `{}` action. Do you want to override it? (y/n)",
described_action.info.to_str(),
))
.prompt()?
{
std::process::exit(0);
}
opts.actions_registry.insert(described_action.clone());
}
Ok(described_action)
}
}
impl UsedAction {
pub fn make_from_defined(
defined_actions: &BTreeSet<DefinedAction>,
variables: &BTreeMap<ShortName, Variable>,
) -> anyhow::Result<Self> {
use inquire_reorder::{Select, Text};
let mut options = vec![];
let mut map = BTreeMap::new();
for item in defined_actions {
let display = format!(
"From project: {}{}",
item.info.to_str(),
if item.tags.is_empty() {
String::new()
} else {
format!(" ({})", item.tags.join(", "))
}
);
options.push(display.clone());
map.insert(display, item);
}
let opt = Select::new("Select a project-defined action to use in a pipeline:", options).prompt()?;
let defined_action = map.get(&opt).unwrap();
let mut used_action = defined_action.r#use();
used_action.title = Text::new("Enter action title (or hit `esc`):").prompt_skippable()?;
if !variables.is_empty() {
used_action = match &defined_action.action {
Action::Custom(cmd) => cmd.prompt_setup_for_project(&used_action, variables)?,
Action::Staged(st) => st.command.prompt_setup_for_project(&used_action, variables)?,
Action::Test(t) => t.command.prompt_setup_for_project(&used_action, variables)?,
Action::Observe(o) => o.command.prompt_setup_for_project(&used_action, variables)?,
_ => used_action,
};
}
Ok(used_action)
}
}
#[allow(dead_code)]
pub fn collect_requirements() -> anyhow::Result<Option<Vec<Requirement>>> {
use inquire_reorder::Confirm;
let mut reqs = Vec::new();
while Confirm::new("Add any requirement (path check or any `Test` action) for this action?")
.with_default(false)
.prompt()?
{
if let Ok(req) = Requirement::new_from_prompt() {
reqs.push(req);
}
}
Ok(if reqs.is_empty() { None } else { Some(reqs) })
}
impl StagedAction {
pub fn new_from_prompt() -> anyhow::Result<Self> {
let stages = vec!["Build", "Deploy"];
let stage = inquire_reorder::Select::new("Select action stage:", stages).prompt()?;
let stage = match stage {
"Build" => Some(Stage::Build),
"Deploy" => Some(Stage::Deploy),
_ => unreachable!(),
};
let command = CustomCommand::new_from_prompt()?;
Ok(StagedAction { stage, command })
}
}
impl TestAction {
pub fn new_from_prompt() -> anyhow::Result<Self> {
let cmd = specify_cmd(None)?;
let placeholders = tags_custom_type("Enter command placeholders, if any:", None).prompt()?;
let env = tags_custom_type("Enter required environment variables, if any:", None).prompt()?;
let ignore_fails = !inquire_reorder::Confirm::new("Does the command failure also means check failure?")
.with_default(true)
.prompt()?;
let mut success_when_found = None;
let mut success_when_not_found = None;
if inquire_reorder::Confirm::new("Specify success when found some regex?")
.with_default(true)
.prompt()?
{
success_when_found = Some(collect_regex("for success on match")?);
}
if inquire_reorder::Confirm::new("Specify success when NOT found some regex?")
.with_default(true)
.prompt()?
{
success_when_not_found = Some(collect_regex("for success on mismatch")?);
}
Ok(Self {
success_when_found,
success_when_not_found,
command: CustomCommand {
cmd,
placeholders,
env,
ignore_fails: if ignore_fails { Some(true) } else { None },
show_success_output: Some(true),
show_cmd: Some(false),
only_when_fresh: None,
remote_exec: Default::default(),
daemon: None,
daemon_wait_seconds: None,
},
})
}
pub fn new_wop_from_prompt() -> anyhow::Result<Self> {
let cmd = specify_cmd(None)?;
let ignore_fails = !inquire_reorder::Confirm::new("Does the command failure also means check failure?")
.with_default(true)
.prompt()?;
let mut success_when_found = None;
let mut success_when_not_found = None;
if inquire_reorder::Confirm::new("Specify success when found some regex?")
.with_default(true)
.prompt()?
{
success_when_found = Some(collect_regex("for success on match")?);
}
if inquire_reorder::Confirm::new("Specify success when NOT found some regex?")
.with_default(true)
.prompt()?
{
success_when_not_found = Some(collect_regex("for success on mismatch")?);
}
Ok(Self {
success_when_found,
success_when_not_found,
command: CustomCommand {
cmd,
placeholders: Default::default(),
env: Default::default(),
ignore_fails: if ignore_fails { Some(true) } else { None },
show_success_output: Some(true),
show_cmd: Some(false),
only_when_fresh: None,
remote_exec: Default::default(),
daemon: None,
daemon_wait_seconds: None,
},
})
}
}
impl PatchAction {
pub fn new_from_prompt() -> anyhow::Result<Self> {
let patch = PathBuf::from(inquire_reorder::Text::new("Specify patch location:").prompt()?);
Ok(Self {
patch,
ignore_fails: None,
})
}
}
impl DescribedPipeline {
pub fn new_from_prompt(
globals: &mut DeployerGlobalConfig,
actions: &mut BTreeSet<DefinedAction>,
) -> anyhow::Result<Self> {
use inquire_reorder::Text;
let info = Text::new("Specify the Pipeline's short name and version:")
.with_placeholder("pipeline-name@0.1.0")
.prompt()?;
let info = PipelineInfo::try_from_str(&info)?;
let name = Text::new("Write a short name for the pipeline:")
.with_placeholder("some-short-name")
.prompt()?;
let desc = Text::new("Write the pipeline's additional description (or hit `esc`):").prompt_skippable()?;
let tags: Vec<String> = tags_custom_type("Write pipeline's tags, if any:", None).prompt()?;
let mut selected_actions = collect_multiple_actions(globals, actions)?;
if !selected_actions.is_empty() {
selected_actions = reorder_actions(selected_actions)?;
}
let exclusive_exec_tag = Text::new("Specify exclusive pipeline tag (or hit `esc`):").prompt_skippable()?;
let artifacts = collect_af_placements()?;
let described_pipeline = DescribedPipeline {
title: name,
desc,
info,
tags,
copy_only: Default::default(),
actions: selected_actions,
default: None,
exclusive_exec_tag,
artifacts,
containered_opts: None,
compose_opts: None,
ansible_opts: None,
systemd_opts: None,
gh_opts: None,
gl_opts: None,
driver: Default::default(),
};
Ok(described_pipeline)
}
}
pub fn collect_multiple_actions(
globals: &mut DeployerGlobalConfig,
project_actions: &mut BTreeSet<DefinedAction>,
) -> anyhow::Result<Vec<UsedAction>> {
use inquire_reorder::Confirm;
let mut actions = Vec::new();
let mut first = true;
while Confirm::new("<<< <<< Add action?").with_default(first).prompt()? {
actions.push(select_action(globals, project_actions)?);
first = false;
}
Ok(actions)
}
pub fn select_action(
globals: &mut DeployerGlobalConfig,
project_actions: &mut BTreeSet<DefinedAction>,
) -> anyhow::Result<UsedAction> {
use inquire_reorder::{Select, Text};
let (actions, keys) = {
let mut h = bmap!();
let mut k = vec![];
for action in project_actions.iter() {
let new_key = action.info.to_str().to_string();
h.insert(new_key.clone(), action.clone());
k.push(new_key);
}
k.sort();
k.push("• Specify another action".to_string());
(h, k)
};
let selected_action = Select::new("Select action for adding to pipeline:", keys).prompt()?;
if selected_action.as_str().eq("• Specify another action") {
let action = new_action(globals, &NewActionArgs { from: None })?;
let title = Some(Text::new("Describe this action inside your pipeline:").prompt()?);
let info = action.info.clone();
project_actions.insert(action);
return Ok(UsedAction {
title,
used: info,
with: Default::default(),
});
}
let action = actions.get(&selected_action).unwrap().clone();
let title = Some(Text::new("Describe this action inside your pipeline:").prompt()?);
let info = action.info.clone();
project_actions.insert(action);
Ok(UsedAction {
title,
used: info,
with: Default::default(),
})
}
pub fn select_pipeline(globals: &mut DeployerGlobalConfig) -> anyhow::Result<DescribedPipeline> {
use inquire_reorder::{Select, Text};
let (actions, keys) = {
let mut h = bmap!();
let mut k = vec![];
for pipeline in globals.pipelines_registry.iter() {
let new_key = format!("{} - {}", pipeline.info.to_str(), pipeline.title);
h.insert(new_key.clone(), pipeline);
k.push(new_key);
}
k.sort();
k.push("• Specify another pipeline".to_string());
(h, k)
};
let selected_action = Select::new("Select a pipeline:", keys).prompt()?;
if selected_action.as_str().eq("• Specify another pipeline") {
let mut pipeline = DescribedPipeline::new_from_prompt(globals, &mut bset![])?;
let new_title = Text::new("Describe this action inside your pipeline:").prompt()?;
pipeline.desc = Some(format!(
r#"Got from `{}`.{}{}"#,
pipeline.title,
if pipeline.desc.as_ref().is_none_or(|d| d.is_empty()) {
""
} else {
" "
},
pipeline.desc.as_deref().unwrap_or("")
));
pipeline.title = new_title;
return Ok(pipeline);
}
let mut action = (*actions.get(&selected_action).unwrap()).clone();
let new_title = Text::new("Describe this action inside your pipeline:").prompt()?;
action.desc = Some(format!(
r#"Got from `{}`.{}{}"#,
action.title,
if action.desc.as_ref().is_none_or(|d| d.is_empty()) {
""
} else {
" "
},
action.desc.as_deref().unwrap_or("")
));
action.title = new_title;
Ok(action)
}
pub fn reorder_actions(selected_actions_unordered: Vec<UsedAction>) -> anyhow::Result<Vec<UsedAction>> {
let mut h = bmap!();
let mut k = vec![];
for selected_action in selected_actions_unordered {
let key = selected_action.used.to_str().to_string();
k.push(key.clone());
h.insert(key, selected_action);
}
let reordered = inquire_reorder::Reorder::new("Reorder pipeline's actions:", k).prompt()?;
let mut selected_actions_ordered = vec![];
for key in reordered {
selected_actions_ordered.push((*h.get(&key).unwrap()).clone());
}
Ok(selected_actions_ordered)
}
pub fn collect_path() -> anyhow::Result<PathBuf> {
Ok(PathBuf::from(inquire_reorder::Text::new("Enter the path:").prompt()?))
}
pub fn collect_paths() -> anyhow::Result<Vec<PathBuf>> {
let mut v = vec![];
let mut first = true;
while inquire_reorder::Confirm::new("Add new path?")
.with_default(first)
.prompt()?
{
v.push(collect_path()?);
first = false;
}
Ok(v)
}
pub fn collect_variables() -> anyhow::Result<BTreeMap<ShortName, Variable>> {
let mut vars = bmap![];
while inquire_reorder::Confirm::new("Add new project-related variable or secret?")
.with_default(false)
.prompt()?
{
let (k, v) = Variable::new_from_prompt()?;
vars.insert(k, v);
}
Ok(vars)
}
pub fn collect_af_placement() -> anyhow::Result<Placement> {
use inquire_reorder::Text;
let from = Text::new("FROM: Enter artifact's path:").prompt()?;
let to = Text::new(" TO: Enter relative path of artifact placement (inside `artifacts` subfolder):").prompt()?;
Ok(Placement {
from,
to,
with: Default::default(),
})
}
pub fn collect_af_placements() -> anyhow::Result<Vec<Placement>> {
use inquire_reorder::Confirm;
let mut v = vec![];
let mut prompt = "Do you want to create artifact placement from run directory to your project's location (inside `artifacts` subfolder)?";
while Confirm::new(prompt).with_default(false).prompt()? {
v.push(collect_af_placement()?);
prompt = "Add one more artifact placement?";
}
Ok(v)
}
pub fn collect_regex(for_what: &str) -> anyhow::Result<String> {
let mut regex_str;
loop {
regex_str = inquire_reorder::Text::new(&format!("Enter regex {for_what} (or enter '/h' for help):")).prompt()?;
if let Err(e) = Regex::new(®ex_str) {
println!("The regex you've written is invalid due to: {e:?}.");
continue;
}
if regex_str.as_str() != "/h" {
break;
}
println!("Guide: `{}`", "Regex Checks for Deployer".blue());
println!(">>> The usage of regex checks in Deployer is simple enough.");
println!(">>> If you want to specify some text that needed to be found, you simply write this text.");
println!(">>> ");
println!(
">>> For finding an info about any supported regex read this: {}",
"https://docs.rs/regex/latest/regex/".blue()
);
println!(
">>> For checks use this: {} (select `Rust` flavor at left side panel)",
"https://regex101.com/".blue(),
);
}
Ok(regex_str)
}
impl Variable {
pub fn new_from_prompt() -> anyhow::Result<(ShortName, Self)> {
let title = ShortName::new(inquire_reorder::Text::new("Enter your variable's title:").prompt()?)?;
println!(
"{}: if variable is a secret, then no command containing this variable will be printed during the run stage.\nAnd if you use HashiCorp KV2 storage, don't forget about these environment variables: `{}`, `{}`.",
"Note".green().italic(),
VAULT_ADDR_ENV.green(),
VAULT_ADDR_TOKEN.green()
);
let is_secret = inquire_reorder::Confirm::new("Is this variable a secret?")
.with_default(false)
.prompt()?;
let types = vec![
"Simple variable",
"Variable from ENV file",
"Variable from environment",
"Variable returned by shell command",
"Variable from HashiCorp Vault KV2 storage",
];
let r#type = inquire_reorder::Select::new("Select the variable type:", types).prompt()?;
let value = match r#type {
"Simple variable" => Variable::new_plain_from_prompt()?,
"Variable from ENV file" => Variable::new_env_file_from_prompt()?,
"Variable from environment" => Variable::new_env_from_prompt()?,
"Variable returned by shell command" => Variable::new_shell_from_prompt()?,
"Variable from HashiCorp Vault KV2 storage" => Variable::new_kv2_from_prompt()?,
_ => unreachable!(),
};
let var = Variable { is_secret, value };
Ok((title, var))
}
pub fn new_plain_from_prompt() -> anyhow::Result<VarValue> {
Ok(VarValue::Plain {
value: inquire_reorder::Text::new("Enter the variable's content:").prompt()?,
})
}
pub fn new_env_from_prompt() -> anyhow::Result<VarValue> {
Ok(VarValue::FromEnvVar {
var_name: inquire_reorder::Text::new("Enter the variable's key:").prompt()?,
})
}
pub fn new_env_file_from_prompt() -> anyhow::Result<VarValue> {
Ok(VarValue::FromEnvFile(FromEnvFile {
env_file_path: PathBuf::from(inquire_reorder::Text::new("Enter the ENV file path:").prompt()?),
key: inquire_reorder::Text::new("Enter the variable's key:").prompt()?,
}))
}
pub fn new_shell_from_prompt() -> anyhow::Result<VarValue> {
Ok(VarValue::FromCmd {
cmd: inquire_reorder::Text::new("Enter shell command to retrieve required variable:").prompt()?,
})
}
pub fn new_kv2_from_prompt() -> anyhow::Result<VarValue> {
println!(
"{}: before run, you must specify two environment variables for the Deployer:",
"Note".green().italic()
);
Ok(VarValue::FromHcVaultKv2(Kv2Paths {
mount_path: inquire_reorder::Text::new("Enter the KV2 mount path:").prompt()?,
secret_path: inquire_reorder::Text::new("Enter the secret's path:").prompt()?,
}))
}
}
impl Requirement {
pub fn new_from_prompt() -> anyhow::Result<Self> {
let types = vec![
"Check some path exists",
"Check any of given paths exists",
"Check some executable available",
"Check the output of given command",
"Check the remote host availability",
];
let r#type = inquire_reorder::Select::new("Select the requirement type:", types).prompt()?;
match r#type {
"Check some path exists" => Requirement::new_exists_from_prompt(),
"Check any of given paths exists" => Requirement::new_exists_any_from_prompt(),
"Check some executable available" => Requirement::new_inpath_from_prompt(),
"Check the output of given command" => Requirement::new_check_from_prompt(),
"Check the remote host availability" => Requirement::new_remote_from_prompt(),
_ => unreachable!(),
}
}
fn new_exists_from_prompt() -> anyhow::Result<Self> {
Ok(Self::Exists {
path: collect_path()?,
desc: collect_requirement_info(),
})
}
fn new_exists_any_from_prompt() -> anyhow::Result<Self> {
Ok(Self::ExistsAny {
paths: collect_paths()?.into_iter().collect::<BTreeSet<_>>(),
desc: collect_requirement_info(),
})
}
fn new_inpath_from_prompt() -> anyhow::Result<Self> {
Ok(Self::InPath {
executable: inquire_reorder::Text::new("Enter the name of executable:").prompt()?,
desc: collect_requirement_info(),
})
}
fn new_check_from_prompt() -> anyhow::Result<Self> {
Ok(Self::CheckSuccess {
action: TestAction::new_wop_from_prompt()?,
desc: collect_requirement_info(),
})
}
fn new_remote_from_prompt() -> anyhow::Result<Self> {
Ok(Self::RemoteAccessibleAndReady {
remote_host_name: ShortName::new(inquire_reorder::Text::new("Specify the remote host's short name:").prompt()?)?,
})
}
}
fn collect_requirement_info() -> String {
inquire_reorder::Text::new("Describe your requirement, installation steps, or leave empty:")
.prompt()
.unwrap_or_default()
}
impl AutoVersionExtractFromRule {
pub fn new_from_prompt() -> anyhow::Result<Self> {
let new_autover_rule = inquire_reorder::Select::new(
"Select the version detection method:",
vec![
"execute the command and get output from `stdout` as version",
"get a version from specified plain file",
],
)
.prompt()?;
let auto_version_rule = match new_autover_rule {
"execute the command and get output from `stdout` as version" => AutoVersionExtractFromRule::CmdStdout {
cmd: {
let mut cmd = CustomCommand::new_observe_from_prompt()?;
cmd.show_success_output = Some(true);
cmd
},
},
"get a version from specified plain file" => AutoVersionExtractFromRule::PlainFile {
path: {
let path = inquire_reorder::Text::new("Specify the relative path to plain version file:").prompt()?;
PathBuf::from(path)
},
},
_ => bail!("There is no such type"),
};
Ok(auto_version_rule)
}
}
impl CustomCommand {
pub fn new_from_prompt() -> anyhow::Result<CustomCommand> {
let cmd = specify_cmd(None)?;
let placeholders = tags_custom_type("Enter command placeholders, if any:", None).prompt()?;
let env = tags_custom_type("Enter required environment variables, if any:", None).prompt()?;
let ignore_fails = inquire_reorder::Confirm::new("Ignore command failures?")
.with_default(false)
.prompt()?;
let show_success_output = inquire_reorder::Confirm::new("Show an output of command if it executed successfully?")
.with_default(false)
.prompt()?;
let show_cmd = inquire_reorder::Confirm::new("Show an entire command at pipeline's run?")
.with_default(true)
.prompt()?;
let only_when_fresh = if !inquire_reorder::Confirm::new("Start a command only in fresh runs?")
.with_default(false)
.prompt()?
{
None
} else {
Some(true)
};
let daemon = if !inquire_reorder::Confirm::new("Run as a daemon until the pipeline is complete?")
.with_default(false)
.prompt()?
{
None
} else {
Some(true)
};
let remote_exec = collect_remote()?;
Ok(CustomCommand {
cmd,
placeholders,
env,
ignore_fails: if ignore_fails { Some(true) } else { None },
show_success_output: if show_success_output { Some(true) } else { None },
show_cmd: if !show_cmd { Some(false) } else { None },
only_when_fresh,
remote_exec,
daemon,
daemon_wait_seconds: None,
})
}
pub fn new_observe_from_prompt() -> anyhow::Result<CustomCommand> {
let cmd = specify_cmd(None)?;
let placeholders = tags_custom_type("Enter command placeholders, if any:", None).prompt()?;
let env = tags_custom_type("Enter required environment variables, if any:", None).prompt()?;
Ok(CustomCommand {
cmd,
placeholders,
env,
ignore_fails: Some(true),
show_success_output: Some(true),
show_cmd: Some(false),
only_when_fresh: Some(false),
remote_exec: Default::default(),
daemon: None,
daemon_wait_seconds: None,
})
}
}
pub fn specify_cmd(default: Option<&str>) -> anyhow::Result<String> {
let mut cmd;
loop {
let mut text_prompt = inquire_reorder::Text::new("Enter a command for terminal (or enter '/h' for help):");
if let Some(default) = default {
text_prompt = text_prompt.with_initial_value(default);
}
cmd = text_prompt.prompt()?;
if cmd.as_str() != "/h" {
break;
}
println!("Guide: `{}`", "Shell Commands for Deployer".blue());
println!(">>> The usage of shell commands in Deployer is very simple.");
println!(
">>> You can use `{}` for home directories, your default `{}` variable and so on.",
"~".green(),
"PATH".green(),
);
println!(">>> ");
println!(">>> Also you can write your commands even when there are some unspecified variables:");
println!(">>> `{}`", "g++ <input-file> -o <output-file>".green());
println!(
">>> `{}{}`",
"docker compose run -e DEPLOY_KEY=".green(),
"{{my very secret key}}".red()
);
println!(">>> ");
println!(">>> To specify shell for Deployer, use `DEPLOYER_SH_PATH` environment variable.");
println!(">>> By default: using `{}`.", "/bin/bash".green());
let shell = match std::env::var("DEPLOYER_SH_PATH") {
Ok(path) => format!("`{}`", path.green()),
Err(_) => format!("\"\" (`{}`)", "/bin/bash".green()),
};
println!(">>> Now: using {}", shell);
}
Ok(cmd)
}
impl RemoteHost {
pub fn new_from_prompt() -> anyhow::Result<Self> {
let short_name = ShortName::new(inquire_reorder::Text::new("Specify the remote host's short name:").prompt()?)?;
let ip = inquire_reorder::Text::new("Specify the IP-address of a remote host's SSH server:")
.prompt()?
.parse()?;
let port = inquire_reorder::Text::new("Specify the port of a remote host's SSH server:")
.prompt()?
.parse()?;
let username = inquire_reorder::Text::new("Specify the remote username:").prompt()?;
let ssh_key_path =
inquire_reorder::Text::new("Specify the path to private SSH key for remote host (or hit `esc`):")
.prompt_skippable()?
.map(PathBuf::from);
Ok(Self {
short_name,
ip,
port,
username,
ssh_private_key_file: ssh_key_path,
})
}
pub fn new_with_args_from_prompt(args: crate::cmd::NewRemoteArgs) -> anyhow::Result<Self> {
let short_name = ShortName::new(match args.short_name {
Some(name) => name,
None => inquire_reorder::Text::new("Specify the remote host's short name:").prompt()?,
})?;
let ip = match args.ip {
Some(ip) => ip,
None => inquire_reorder::Text::new("Specify the IP-address of a remote host's SSH server:")
.prompt()?
.parse()?,
};
let port = match args.port {
Some(port) => port,
None => inquire_reorder::Text::new("Specify the port of a remote host's SSH server:")
.prompt()?
.parse()?,
};
let username = match args.username {
Some(username) => username,
None => inquire_reorder::Text::new("Specify the remote username:").prompt()?,
};
let ssh_key_path = match args.ssh_key_path {
Some(path) => Some(path),
None => inquire_reorder::Text::new("Specify the path to private SSH key for remote host (or hit `esc`):")
.prompt_skippable()?
.map(PathBuf::from),
};
Ok(Self {
short_name,
ip,
port,
username,
ssh_private_key_file: ssh_key_path,
})
}
}
fn collect_remote() -> anyhow::Result<Vec<ShortName>> {
const PROMPT: &str = "Do you want to execute this command remotely on one or many remote hosts? If yes, the command won't be executed on this host.";
const ANOTHER_PROMPT: &str = "Add one more remote host?";
let mut v = vec![];
let mut prompt = PROMPT;
while inquire_reorder::Confirm::new(prompt).with_default(false).prompt()? {
v.push(ShortName::new(
inquire_reorder::Text::new("Specify the remote host's short name:").prompt()?,
)?);
prompt = ANOTHER_PROMPT;
}
Ok(v)
}
fn collect_remotes(remotes: &BTreeMap<ShortName, RemoteHost>) -> anyhow::Result<BTreeSet<ShortName>> {
use inquire_reorder::Confirm;
let mut set = bset! {};
let mut r#default = true;
while Confirm::new("Add remote host?").with_default(r#default).prompt()? {
set.insert(
crate::remote::choose_remote(remotes, "Choose a remote host:")?
.0
.to_owned(),
);
r#default = false;
}
Ok(set)
}
impl AnsibleOpts {
pub fn new_from_prompt(remotes: &BTreeMap<ShortName, RemoteHost>) -> anyhow::Result<Self> {
use inquire_reorder::{Confirm, Text};
if Confirm::new("Do you have an inventory in INI format? (if no, then hit `n` to choose remotes from Registry)")
.with_default(false)
.prompt()?
{
let inventory_path = PathBuf::from(
Text::new("Enter the path to inventory:")
.with_placeholder("path/to/inventory.ini")
.prompt()?,
);
let mut host_group = Text::new("Enter related host group:")
.with_initial_value("all")
.prompt_skippable()?;
if let Some("all") = host_group.as_deref() {
host_group = None;
}
Ok(Self {
use_inventory: Some(inventory_path),
host_group,
..Default::default()
})
} else {
Ok(Self {
create_inventory: collect_remotes(remotes)?,
..Default::default()
})
}
}
}
impl ContaineredOpts {
pub fn new_from_prompt(driver: PipelineDriver) -> anyhow::Result<Self> {
use inquire_reorder::Text;
let mut base_image = Text::new("Specify the base image:")
.with_placeholder("image:version")
.with_initial_value(crate::containered::BASE_IMAGE)
.prompt_skippable()?;
if let Some(crate::containered::BASE_IMAGE) = base_image.as_deref() {
base_image = None;
}
let depl_build_image = if driver == PipelineDriver::Deployer {
let mut depl_build_image = Text::new("")
.with_placeholder("image:version")
.with_initial_value(base_image.as_deref().unwrap_or(crate::containered::BASE_IMAGE))
.prompt_skippable()?;
if let Some(crate::containered::BASE_IMAGE) = depl_build_image.as_deref() {
depl_build_image = None;
}
depl_build_image
} else {
None
};
println!("Other options can be edited via `$ depl edit project`.");
Ok(Self {
base_image,
build_deployer_base_image: depl_build_image,
..Default::default()
})
}
}
impl PortBinding {
pub fn new_from_prompt() -> anyhow::Result<Self> {
let from: u16 = inquire_reorder::Text::new("Enter `from` port:").prompt()?.parse()?;
let to: u16 = inquire_reorder::Text::new("Enter `to` port:").prompt()?.parse()?;
Ok(Self { from, to })
}
}
impl ComposeServiceOpts {
pub fn new_from_prompt() -> anyhow::Result<(String, Self)> {
use inquire_reorder::Text;
let name = Text::new("Enter service name:").with_placeholder("postgres").prompt()?;
let image = Text::new("Enter Docker image for this service:")
.with_placeholder("postgres:16")
.prompt()?;
println!("Other service options can be edited via `$ depl edit project`.");
Ok((
name,
Self {
image,
..Default::default()
},
))
}
}
impl ComposeOpts {
pub fn new_from_prompt(driver: PipelineDriver) -> anyhow::Result<Self> {
println!("Setting up the main application service (same as containered options):");
let app = ContaineredOpts::new_from_prompt(driver)?;
let mut services = BTreeMap::new();
while inquire_reorder::Confirm::new("Add a service (e.g. postgres, redis)?")
.with_default(services.is_empty())
.prompt()?
{
let (name, service) = ComposeServiceOpts::new_from_prompt()?;
services.insert(name, service);
}
println!("Other compose options can be edited via `$ depl edit project`.");
Ok(Self {
app,
services,
..Default::default()
})
}
}