#![forbid(unsafe_code)]
use crate::cli::path_parse::{parse_exec_target, ExecTargetError, ExecTargetPlan};
use crate::errors::SshCliError;
use anyhow::Result;
use std::path::Path;
pub(crate) struct ExecTargetArgs {
pub(crate) all: bool,
pub(crate) hosts: Option<String>,
pub(crate) tags: Option<String>,
pub(crate) use_active: bool,
pub(crate) target: Vec<String>,
}
pub(crate) fn resolve_exec_target(
args: ExecTargetArgs,
config_override: Option<&Path>,
) -> Result<ExecTargetPlan> {
let active = crate::vps::read_active_vps(config_override)?;
let use_active = args.use_active;
let plan = parse_exec_target(
args.all,
args.hosts,
args.tags,
use_active,
args.target,
active,
)
.map_err(|e| match e {
ExecTargetError::NoActiveVps => SshCliError::NoActiveVps,
ExecTargetError::Invalid(s) => SshCliError::InvalidArgument(s),
})?;
if use_active && crate::vps::find_by_name(config_override, &plan.command)?.is_some() {
return Err(SshCliError::InvalidArgument(format!(
"`{name}` is a registered VPS, not a command; \
drop --use-active and pass `{name} <COMMAND>`",
name = plan.command
))
.into());
}
Ok(plan)
}
pub(crate) struct HealthTargetArgs {
pub(crate) all: bool,
pub(crate) hosts: Option<String>,
pub(crate) use_active: bool,
pub(crate) vps_name: Option<String>,
}
pub(crate) fn resolve_health_target(
args: HealthTargetArgs,
config_override: Option<&Path>,
) -> Result<(crate::vps::HostSelection, crate::json_wire::TargetSource)> {
use crate::domain::VpsName;
use crate::json_wire::TargetSource;
use crate::vps::HostSelection;
let invalid = |s: &str| -> anyhow::Error { SshCliError::InvalidArgument(s.to_string()).into() };
let refine = |n: String| -> Result<VpsName> {
VpsName::try_new(n).map_err(|e| SshCliError::InvalidArgument(e.to_string()).into())
};
if args.all {
return Ok((HostSelection::All, TargetSource::Selector));
}
if let Some(h) = args.hosts {
let names = crate::cli::path_parse::parse_hosts_list(&h);
if names.is_empty() {
return Err(invalid("--hosts requires at least one host name"));
}
let names = names.into_iter().map(refine).collect::<Result<Vec<_>>>()?;
return Ok((HostSelection::Named(names), TargetSource::Selector));
}
if let Some(n) = args.vps_name {
return Ok((HostSelection::Single(refine(n)?), TargetSource::Argv));
}
if args.use_active {
let active = crate::vps::read_active_vps(config_override)?;
let name = active.ok_or(SshCliError::NoActiveVps)?;
return Ok((
HostSelection::Single(refine(name)?),
TargetSource::ActiveMarker,
));
}
Err(invalid(HEALTH_USAGE))
}
pub(crate) const HEALTH_USAGE: &str = concat!(
"designate the target explicitly: `<VPS>`, ",
"or a selector (`--all`/`--hosts <LIST>`), ",
"or the active marker deliberately (`--use-active`)"
);
pub(crate) struct ExecCommonArgs {
pub(crate) steps: Vec<String>,
pub(crate) auth: crate::cli::SshAuthArgs,
pub(crate) elevation_password: Option<secrecy::SecretString>,
pub(crate) elevation: Elevation,
pub(crate) timeout: Option<u64>,
pub(crate) global_timeout: Option<u64>,
pub(crate) description: Option<String>,
pub(crate) replace_host_key: bool,
pub(crate) disable_sudo: bool,
pub(crate) target_source: crate::json_wire::TargetSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Elevation {
None,
Sudo,
Su,
}
pub(crate) fn build_exec_options(c: ExecCommonArgs) -> Result<crate::vps::ExecOptions> {
let ExecCommonArgs {
steps,
auth,
elevation_password,
elevation,
timeout,
global_timeout,
description,
replace_host_key,
disable_sudo,
target_source,
} = c;
let key = auth.key_path_string();
let password = crate::cli::read_stdin_if(auth.password_stdin, auth.password.clone())?;
let key_passphrase =
crate::cli::read_stdin_if(auth.key_passphrase_stdin, auth.key_passphrase.clone())?;
let steps = crate::cli::parse_remote_steps(steps).map_err(SshCliError::InvalidArgument)?;
let (sudo_password, su_password) = match elevation {
Elevation::None => (None, None),
Elevation::Sudo => (elevation_password, None),
Elevation::Su => (None, elevation_password),
};
Ok(crate::vps::ExecOptions {
password,
sudo_password,
su_password,
key,
key_passphrase,
timeout: crate::cli::effective_timeout_ms(timeout, global_timeout)
.map_err(SshCliError::InvalidArgument)?,
description,
replace_host_key,
disable_sudo,
steps,
target_source,
use_agent: auth.use_agent,
agent_socket: auth
.agent_socket
.as_ref()
.map(|p| p.to_string_lossy().into_owned()),
})
}
#[cfg(test)]
mod tests;