use isothrum_runner::{AccessPick, Endpoint, Events, Occupied, Op, Park, RunName, RunOptions, Runner, Running, ShellCommand};
use std::borrow::Borrow;
use crate::args::{Cli, Command, Lifecycle, Mode, OpArgs, RunArgs, RunnerCommand};
use crate::error::{Error, Result};
use crate::exit::Verdict;
use crate::term::{Human, Json, Term, Traced, Trail};
use crate::text::{human, status_text};
use crate::values::Var;
use crate::{locate, values};
pub(crate) async fn run<R: Borrow<Runner>>(cli: &Cli, runner: impl FnOnce() -> Result<R>, term: &Term, trail: &Trail, var: &Var<'_>) -> Result<Verdict> {
let command = match &cli.command {
Command::Completions { shell } => {
term.out(&crate::args::completions(*shell));
return Ok(Verdict::Pass);
}
Command::Runner(command) => command,
};
let runner = runner()?;
let (json, human) = (Json(term), Human(term));
let sink: &dyn Events = if cli.json { &json } else { &human };
let traced = Traced { sink, trail };
let out = Out {
json: cli.json,
term,
events: &traced,
};
on_runner(command, runner.borrow(), &out, var).await
}
async fn on_runner(command: &RunnerCommand, runner: &Runner, out: &Out<'_>, var: &Var<'_>) -> Result<Verdict> {
let Out { term, events, .. } = *out;
match command {
RunnerCommand::Status(run) => status(out, &open(runner, run)?).await,
RunnerCommand::Check(op) => check(out, &open(runner, &op.run)?, &options(op, var)?).await,
RunnerCommand::Provision { op, machine } => {
let run = open(runner, &op.run)?;
Ok(Verdict::of(run.provision(machine.as_deref(), &options(op, var)?, events).await.is_ok()))
}
RunnerCommand::Attach { op, machine, network, image } => {
let mut pick = AccessPick::default();
if let Some(m) = machine {
pick = pick.with_machine(m);
}
if let Some(n) = network {
pick = pick.with_network(n);
}
if let Some(i) = image {
pick = pick.with_image(i);
}
attach(out, &open(runner, &op.run)?, pick, &options(op, var)?).await
}
RunnerCommand::Detach { op, machine } => {
let run = open(runner, &op.run)?;
Ok(Verdict::of(run.detach(machine.as_deref(), &options(op, var)?, events).await.is_ok()))
}
RunnerCommand::Console { run, machine, user } => print_command(out, &open(runner, run)?.console_command(machine, user.as_deref())?),
RunnerCommand::Screen { run, machine, show_password } => screen(out, &open(runner, run)?, machine, *show_password).await,
RunnerCommand::Events(run) => print_events(out, runner, &open(runner, run)?),
RunnerCommand::Reap(args) => reap::run(args, runner, out, var).await,
RunnerCommand::Key => {
let key = runner.ssh_public_key().await?;
if out.json {
term.out(&serde_json::json!({ "public_key": key }).to_string());
} else {
term.out(&key);
}
Ok(Verdict::Pass)
}
RunnerCommand::Lifecycle(command) => lifecycle(command, runner, out, var).await,
}
}
pub(crate) fn op_of(command: &Command) -> Option<Op> {
let Command::Runner(command) = command else { return None };
Some(match &**command {
RunnerCommand::Lifecycle(Lifecycle::Up { .. }) => Op::Up,
RunnerCommand::Lifecycle(Lifecycle::Down(_)) => Op::Down,
RunnerCommand::Lifecycle(Lifecycle::Park { .. }) => Op::Park,
RunnerCommand::Lifecycle(Lifecycle::Resume(_)) => Op::Resume,
RunnerCommand::Lifecycle(Lifecycle::Update(_)) => Op::Update,
RunnerCommand::Lifecycle(Lifecycle::Retire { .. }) => Op::Retire,
RunnerCommand::Check(_) => Op::Check,
RunnerCommand::Provision { .. } => Op::Provision,
RunnerCommand::Attach { .. } => Op::Attach,
RunnerCommand::Detach { .. } => Op::Detach,
RunnerCommand::Status(_)
| RunnerCommand::Console { .. }
| RunnerCommand::Screen { .. }
| RunnerCommand::Events(_)
| RunnerCommand::Reap(_)
| RunnerCommand::Key => {
return None;
}
})
}
pub(crate) fn run_named(command: &Command) -> Option<&str> {
let Command::Runner(command) = command else { return None };
let target = match &**command {
RunnerCommand::Lifecycle(
Lifecycle::Up { op, .. }
| Lifecycle::Down(op)
| Lifecycle::Park { op, .. }
| Lifecycle::Resume(op)
| Lifecycle::Update(op)
| Lifecycle::Retire { op, .. },
)
| RunnerCommand::Check(op)
| RunnerCommand::Provision { op, .. }
| RunnerCommand::Attach { op, .. }
| RunnerCommand::Detach { op, .. } => &op.run,
RunnerCommand::Status(run) | RunnerCommand::Console { run, .. } | RunnerCommand::Screen { run, .. } | RunnerCommand::Events(run) => run,
RunnerCommand::Reap(_) | RunnerCommand::Key => return None,
};
target.name.as_deref()
}
struct Out<'a> {
json: bool,
term: &'a Term,
events: &'a dyn Events,
}
async fn lifecycle(command: &Lifecycle, runner: &Runner, out: &Out<'_>, var: &Var<'_>) -> Result<Verdict> {
let Out { json, term, events } = *out;
match command {
Lifecycle::Up {
op,
replace,
lifetime,
ssh_jump,
} => {
let started = open(runner, &op.run)?
.up(&start_opts(op, *replace, *lifetime, ssh_jump.as_deref(), var)?, events)
.await;
Ok(said(json, started.ok(), "The run is up.", term))
}
Lifecycle::Down(op) => Ok(Verdict::of(open(runner, &op.run)?.down(&options(op, var)?, events).await.is_ok())),
Lifecycle::Park { op, mode } => {
let parked = open(runner, &op.run)?.park(park_mode(*mode), &options(op, var)?, events).await;
Ok(Verdict::of(parked.is_ok()))
}
Lifecycle::Resume(op) => {
let resumed = open(runner, &op.run)?.resume(&options(op, var)?, events).await;
Ok(said(json, resumed.ok(), "The run is up again.", term))
}
Lifecycle::Update(op) => {
let updated = open(runner, &op.run)?.update(&options(op, var)?, events).await;
Ok(said(json, updated.ok(), "The run is up to date.", term))
}
Lifecycle::Retire { op, machine } => {
let retired = open(runner, &op.run)?.retire(machine, &options(op, var)?, events).await;
Ok(Verdict::of(retired.is_ok()))
}
}
}
fn park_mode(mode: Mode) -> Park {
match mode {
Mode::Pause => Park::Pause,
Mode::Shutdown => Park::Shutdown,
}
}
fn said(json: bool, run: Option<isothrum_runner::Started>, done: &str, term: &Term) -> Verdict {
let Some(run) = run else { return Verdict::Fail };
if !json {
term.out(run.message.as_deref().unwrap_or(done).trim_end());
}
Verdict::Pass
}
async fn status(out: &Out<'_>, run: &Running<'_>) -> Result<Verdict> {
let Out { json, term, .. } = *out;
let status = run.status().await?;
if json {
term.out(&serde_json::to_string(&status)?);
} else {
for line in status_text(run.name().as_str(), &status) {
term.out(&line);
}
}
Ok(Verdict::Pass)
}
async fn attach(out: &Out<'_>, run: &Running<'_>, pick: AccessPick<'_>, opts: &RunOptions) -> Result<Verdict> {
let Out { json, term, events } = *out;
let Ok(report) = run.attach(pick, opts, events).await else {
return Ok(Verdict::Fail);
};
if !json {
let at = report.address.as_deref().map(|a| format!(" at {a}")).unwrap_or_default();
term.out(&format!("Attached {}{at}.", report.machine));
}
Ok(Verdict::Pass)
}
fn print_command(out: &Out<'_>, cmd: &ShellCommand) -> Result<Verdict> {
let Out { json, term, .. } = *out;
if json {
term.out(&serde_json::to_string(cmd)?);
} else {
term.out(&cmd.sh_line());
}
Ok(Verdict::Pass)
}
async fn screen(out: &Out<'_>, run: &Running<'_>, machine: &str, show: bool) -> Result<Verdict> {
let Out { json, term, .. } = *out;
let console = run.graphical_console(machine).await?;
let protocol = serde_json::to_value(console.protocol)?.as_str().unwrap_or_default().to_owned();
let (address, websocket) = match &console.endpoint {
Endpoint::Loopback(at) => (Some(at.to_string()), None),
Endpoint::ProxmoxVnc(vnc) => (None, Some(vnc.websocket.expose().to_owned())),
_ => (None, None),
};
let password = console.password.as_ref().filter(|_| show).map(|p| p.expose().to_owned());
if json {
let line = serde_json::json!({
"machine": console.machine, "protocol": protocol, "address": address, "websocket": websocket,
"user": console.user, "password": password, "tunnelled": console.tunnelled(), "note": console.note,
});
term.out(&line.to_string());
} else {
term.out(&format!("{protocol} {}", address.or(websocket).unwrap_or_default()));
if let Some(user) = &console.user {
term.out(&format!("user: {user}"));
}
match (&password, &console.password) {
(Some(p), _) => term.out(&format!("password: {p}")),
(None, Some(_)) => term.out("password: (hidden: --show-password prints it)"),
(None, None) => {}
}
if console.tunnelled() {
term.out("The tunnel stays open until this command is stopped (Ctrl-C).");
}
}
if console.tunnelled() {
std::future::pending::<()>().await;
}
Ok(Verdict::Pass)
}
async fn check(out: &Out<'_>, run: &Running<'_>, opts: &RunOptions) -> Result<Verdict> {
let Out { json, term, events } = *out;
let Ok(report) = run.check(opts, events).await else {
return Ok(Verdict::Fail);
};
if !json {
let failed = report.results.iter().filter(|r| !r.ok).count();
term.out(&format!("{} passed, {failed} failed", report.results.len() - failed));
}
Ok(Verdict::of(report.ok))
}
fn print_events(out: &Out<'_>, runner: &Runner, run: &Running<'_>) -> Result<Verdict> {
let Out { json, term, .. } = *out;
let log = runner.events(run.name())?;
for event in log.events {
if json {
term.out(&serde_json::to_string(&event)?);
} else if let Some(line) = human(&event.kind) {
let op = event.op.map_or("", Op::as_str);
term.out(&format!("{} {op:<6} {line}", event.time));
}
}
if log.unreadable > 0 {
term.err(&format!(
"warning: {} line(s) of run `{}`'s event log couldn't be read (written by another version of Isothrum, or cut short)",
log.unreadable,
run.name()
));
}
Ok(Verdict::Pass)
}
fn open<'r>(runner: &'r Runner, args: &RunArgs) -> Result<Running<'r>> {
let manifest = locate::manifest(&args.path, args.target.as_deref())?;
let name = args
.name
.as_deref()
.map(RunName::new)
.transpose()
.map_err(|e| Error::Usage(format!("--name: {e}")))?;
Ok(runner.open(&manifest, name)?)
}
fn start_opts(args: &OpArgs, replace: bool, lifetime: Option<u64>, ssh_jump: Option<&str>, var: &Var<'_>) -> Result<RunOptions> {
let mut opts = options(args, var)?;
if let Some(jump) = ssh_jump {
opts = opts.with_ssh_jump(jump);
}
if replace {
opts = opts.with_occupied(Occupied::Replace);
}
if let Some(minutes) = lifetime {
opts = opts.with_lifetime(std::time::Duration::from_secs(minutes.saturating_mul(60)));
}
Ok(opts)
}
fn options(args: &OpArgs, var: &Var<'_>) -> Result<RunOptions> {
let mut opts = RunOptions::new()
.with_params(values::params(&args.params)?)
.with_secrets(values::secrets(&args.secrets, var)?);
if let Some(label) = &args.host_label {
opts = opts.with_host_label(label);
}
if let Some(provider) = args.provider {
opts = opts.with_provider(provider);
}
Ok(opts)
}
mod reap;
#[cfg(test)]
mod tests;