depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! Remote module.

use anyhow::{anyhow, bail};
use colored::Colorize;
use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf};

use crate::cmd::{CatRemoteArgs, EditRemoteArgs, NewRemoteArgs, RemoveRemoteArgs};
use crate::entities::info::ShortName;
use crate::entities::remote_host::RemoteHost;
use crate::globals::DeployerGlobalConfig;

/// List available remote hosts.
pub fn list_remote(globals: &DeployerGlobalConfig) {
  println!("Known remote hosts in Deployer's Registry:");

  for host in globals.remote_hosts.values() {
    println!("• Host: `{}`", host.short_name.as_str().green().italic());
  }
}

/// Creates a new remote host info.
pub fn new_remote(globals: &mut DeployerGlobalConfig, args: NewRemoteArgs) -> anyhow::Result<RemoteHost> {
  let remote = RemoteHost::new_with_args_from_prompt(args)?;
  globals.remote_hosts.insert(remote.short_name.clone(), remote.clone());

  Ok(remote)
}

/// Chooses remote host from the Registry.
pub fn choose_remote<'a>(
  remotes_registry: &'a BTreeMap<ShortName, RemoteHost>,
  prompt: &str,
) -> anyhow::Result<(&'a ShortName, &'a RemoteHost)> {
  if remotes_registry.is_empty() {
    bail!("There is no remote hosts in the Registry.");
  }

  let keys = remotes_registry
    .keys()
    .map(|short_name| short_name.as_str())
    .collect::<Vec<_>>();
  let selected = inquire_reorder::Select::new(prompt, keys).prompt()?;
  remotes_registry
    .iter()
    .find(|(short_name, _)| short_name.as_str().eq(selected))
    .ok_or(anyhow!("No such remote host!"))
}

/// Prints remote host info.
pub fn cat_remote(globals: &DeployerGlobalConfig, args: CatRemoteArgs) -> anyhow::Result<()> {
  let (short_name, remote_host) = if let Some(short_name) = args.short_name {
    globals
      .remote_hosts
      .iter()
      .find(|(name, _)| name.as_str().eq(&short_name))
      .ok_or(anyhow!("This remote host is not found in registry!"))?
  } else {
    choose_remote(&globals.remote_hosts, "Select remote host for displaying:")?
  };

  println!("Remote host short name: {}", short_name.as_str());
  println!("IP-address: {}", remote_host.ip);
  println!("Port: {}", remote_host.port);
  println!("Username: {}", remote_host.username);

  Ok(())
}

/// Edits remote host info.
pub fn edit_remote(globals: &mut DeployerGlobalConfig, args: EditRemoteArgs) -> anyhow::Result<()> {
  let (short_name, remote_host) = if let Some(short_name) = args.short_name {
    globals
      .remote_hosts
      .iter()
      .find(|(name, _)| name.as_str().eq(&short_name))
      .ok_or(anyhow!("This remote host is not found in registry!"))?
  } else {
    choose_remote(&globals.remote_hosts, "Select remote host for displaying:")?
  };
  let (short_name, mut remote_host) = (short_name.clone(), remote_host.clone());

  remote_host.edit_from_prompt()?;
  globals.remote_hosts.remove(&short_name);
  globals.remote_hosts.insert(short_name, remote_host);

  Ok(())
}

/// Removes remote host info.
pub fn remove_remote(globals: &mut DeployerGlobalConfig, args: RemoveRemoteArgs) -> anyhow::Result<()> {
  use inquire_reorder::Confirm;

  let (short_name, _) = if let Some(short_name) = args.short_name {
    globals
      .remote_hosts
      .iter()
      .find(|(name, _)| name.as_str().eq(&short_name))
      .ok_or(anyhow!("This remote host is not found in registry!"))?
  } else {
    choose_remote(&globals.remote_hosts, "Select remote host for displaying:")?
  };
  let short_name = short_name.clone();

  if !args.yes && !Confirm::new("Are you sure? (y/n)").prompt()? {
    return Ok(());
  }

  globals.remote_hosts.remove(&short_name);

  Ok(())
}

/// Sends build dir without `ignore` files to the remote host.
pub fn sync_to_remote(build_dir: &Path, remote: &RemoteHost, ignore: &HashSet<PathBuf>) -> anyhow::Result<PathBuf> {
  let ignore = ignore
    .iter()
    .map(|p| format!("--exclude='{}'", p.to_string_lossy()))
    .collect::<Vec<_>>()
    .join(" ");

  let mut remote_build_folder = PathBuf::from("~");
  remote_build_folder.push(".cache");
  remote_build_folder.push(crate::CACHE_DIR);

  let build_pathbuf = build_dir.to_path_buf();
  let folder_name = build_pathbuf
    .file_name()
    .unwrap()
    .to_str()
    .expect("`folder_name` contains non-UTF8 symbols!");
  remote_build_folder.push(folder_name);

  let bash_c = format!(
    r#"rsync -avz {} --rsh='ssh -p{}' . "{}@{}:{}""#,
    ignore,
    remote.port,
    remote.username,
    remote.ip,
    remote_build_folder.to_string_lossy(),
  );

  let shell = match std::env::var("DEPLOYER_SH_PATH") {
    Ok(path) => path,
    Err(_) => "/bin/bash".to_string(),
  };

  let mut cmd = std::process::Command::new(&shell);
  cmd
    .current_dir(build_dir)
    .arg("-c")
    .arg(bash_c)
    .stdout(std::process::Stdio::piped())
    .stderr(std::process::Stdio::piped());
  let res = cmd.spawn()?.wait_with_output()?;
  if !res.status.success() {
    let stdout_strs = String::from_utf8_lossy(&res.stdout).to_string();
    let stderr_strs = String::from_utf8_lossy(&res.stderr).to_string();
    bail!("{}{}", stdout_strs, stderr_strs)
  }

  Ok(remote_build_folder)
}

/// Gets the build folder from the remote host to this.
pub fn sync_from_remote(build_dir: &Path, remote: &RemoteHost) -> anyhow::Result<()> {
  let mut remote_build_folder = PathBuf::from("~");
  remote_build_folder.push(".cache");
  remote_build_folder.push(crate::CACHE_DIR);

  let build_pathbuf = build_dir.to_path_buf();
  let folder_name = build_pathbuf
    .file_name()
    .unwrap()
    .to_str()
    .expect("`folder_name` contains non-UTF8 symbols!");
  remote_build_folder.push(folder_name);

  let bash_c = format!(
    r#"rsync -avz --rsh='ssh -p{}' "{}@{}:{}" {:?}"#,
    remote.port,
    remote.username,
    remote.ip,
    remote_build_folder.to_string_lossy(),
    build_dir,
  );

  let shell = match std::env::var("DEPLOYER_SH_PATH") {
    Ok(path) => path,
    Err(_) => "/bin/bash".to_string(),
  };

  let mut cmd = std::process::Command::new(&shell);
  cmd
    .current_dir(build_dir)
    .arg("-c")
    .arg(bash_c)
    .stdout(std::process::Stdio::piped())
    .stderr(std::process::Stdio::piped());
  let res = cmd.spawn()?.wait_with_output()?;
  if !res.status.success() {
    let stdout_strs = String::from_utf8_lossy(&res.stdout).to_string();
    let stderr_strs = String::from_utf8_lossy(&res.stderr).to_string();
    bail!("{}{}", stdout_strs, stderr_strs)
  }

  Ok(())
}

/// Gets build artifacts from the remote host to this host.
pub fn sync_artifacts_from_remote(
  remote_build_dir: &Path,
  artifacts_dir: &Path,
  remote: &RemoteHost,
) -> anyhow::Result<()> {
  let mut artifacts_pathbuf = artifacts_dir.to_path_buf();
  artifacts_pathbuf.push(remote.short_name.as_str());
  std::fs::create_dir_all(&artifacts_pathbuf)?;

  let bash_c = format!(
    r#"rsync -avz --rsh='ssh -p{}' "{}@{}:{}" {:?}"#,
    remote.port,
    remote.username,
    remote.ip,
    remote_build_dir.join(crate::ARTIFACTS_DIR).to_string_lossy(),
    artifacts_pathbuf,
  );

  let shell = match std::env::var("DEPLOYER_SH_PATH") {
    Ok(path) => path,
    Err(_) => "/bin/bash".to_string(),
  };

  let mut cmd = std::process::Command::new(&shell);
  cmd
    .arg("-c")
    .arg(bash_c)
    .stdout(std::process::Stdio::piped())
    .stderr(std::process::Stdio::piped());
  let res = cmd.spawn()?.wait_with_output()?;
  if !res.status.success() {
    let stdout_strs = String::from_utf8_lossy(&res.stdout).to_string();
    let stderr_strs = String::from_utf8_lossy(&res.stderr).to_string();
    bail!("{}{}", stdout_strs, stderr_strs)
  }

  Ok(())
}