From 70c55363a91572790ba5d49b70c58040f112e55c Mon Sep 17 00:00:00 2001 From: notgne2 Date: Fri, 8 Jan 2021 18:24:04 -0700 Subject: Restructure project --- src/activate.rs | 471 -------------------------------------------------------- 1 file changed, 471 deletions(-) delete mode 100644 src/activate.rs (limited to 'src/activate.rs') diff --git a/src/activate.rs b/src/activate.rs deleted file mode 100644 index 49d16af..0000000 --- a/src/activate.rs +++ /dev/null @@ -1,471 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Serokell -// SPDX-FileCopyrightText: 2020 Andreas Fuchs -// -// SPDX-License-Identifier: MPL-2.0 - -use signal_hook::{consts::signal::SIGHUP, iterator::Signals}; - -use clap::Clap; - -use tokio::fs; -use tokio::process::Command; -use tokio::sync::mpsc; -use tokio::time::timeout; - -use std::time::Duration; - -use std::path::Path; - -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; - -use thiserror::Error; - -#[macro_use] -extern crate log; - -#[macro_use] -extern crate serde_derive; - -#[macro_use] -mod utils; - -/// Remote activation utility for deploy-rs -#[derive(Clap, Debug)] -#[clap(version = "1.0", author = "Serokell ")] -struct Opts { - /// Print debug logs to output - #[clap(short, long)] - debug_logs: bool, - /// Directory to print logs to - #[clap(long)] - log_dir: Option, - - /// Path for any temporary files that may be needed during activation - #[clap(long)] - temp_path: String, - - #[clap(subcommand)] - subcmd: SubCommand, -} - -#[derive(Clap, Debug)] -enum SubCommand { - Activate(ActivateOpts), - Wait(WaitOpts), -} - -/// Activate a profile -#[derive(Clap, Debug)] -struct ActivateOpts { - /// The closure to activate - closure: String, - /// The profile path to install into - profile_path: String, - - /// Maximum time to wait for confirmation after activation - #[clap(long)] - confirm_timeout: u16, - - /// Wait for confirmation after deployment and rollback if not confirmed - #[clap(long)] - magic_rollback: bool, - - /// Auto rollback if failure - #[clap(long)] - auto_rollback: bool, -} - -/// Activate a profile -#[derive(Clap, Debug)] -struct WaitOpts { - /// The closure to wait for - closure: String, -} - -#[derive(Error, Debug)] -pub enum DeactivateError { - #[error("Failed to execute the rollback command: {0}")] - RollbackError(std::io::Error), - #[error("The rollback resulted in a bad exit code: {0:?}")] - RollbackExitError(Option), - #[error("Failed to run command for listing generations: {0}")] - ListGenError(std::io::Error), - #[error("Command for listing generations resulted in a bad exit code: {0:?}")] - ListGenExitError(Option), - #[error("Error converting generation list output to utf8: {0}")] - DecodeListGenUtf8Error(#[from] std::string::FromUtf8Error), - #[error("Failed to run command for deleting generation: {0}")] - DeleteGenError(std::io::Error), - #[error("Command for deleting generations resulted in a bad exit code: {0:?}")] - DeleteGenExitError(Option), - #[error("Failed to run command for re-activating the last generation: {0}")] - ReactivateError(std::io::Error), - #[error("Command for re-activating the last generation resulted in a bad exit code: {0:?}")] - ReactivateExitError(Option), -} - -pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { - warn!("De-activating due to error"); - - let nix_env_rollback_exit_status = Command::new("nix-env") - .arg("-p") - .arg(&profile_path) - .arg("--rollback") - .status() - .await - .map_err(DeactivateError::RollbackError)?; - - match nix_env_rollback_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::RollbackExitError(a)), - }; - - debug!("Listing generations"); - - let nix_env_list_generations_out = Command::new("nix-env") - .arg("-p") - .arg(&profile_path) - .arg("--list-generations") - .output() - .await - .map_err(DeactivateError::ListGenError)?; - - match nix_env_list_generations_out.status.code() { - Some(0) => (), - a => return Err(DeactivateError::ListGenExitError(a)), - }; - - let generations_list = String::from_utf8(nix_env_list_generations_out.stdout)?; - - let last_generation_line = generations_list - .lines() - .last() - .expect("Expected to find a generation in list"); - - let last_generation_id = last_generation_line - .split_whitespace() - .next() - .expect("Expected to get ID from generation entry"); - - debug!("Removing generation entry {}", last_generation_line); - warn!("Removing generation by ID {}", last_generation_id); - - let nix_env_delete_generation_exit_status = Command::new("nix-env") - .arg("-p") - .arg(&profile_path) - .arg("--delete-generations") - .arg(last_generation_id) - .status() - .await - .map_err(DeactivateError::DeleteGenError)?; - - match nix_env_delete_generation_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::DeleteGenExitError(a)), - }; - - info!("Attempting to re-activate the last generation"); - - let re_activate_exit_status = Command::new(format!("{}/deploy-rs-activate", profile_path)) - .env("PROFILE", &profile_path) - .current_dir(&profile_path) - .status() - .await - .map_err(DeactivateError::ReactivateError)?; - - match re_activate_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::ReactivateExitError(a)), - }; - - Ok(()) -} - -#[derive(Error, Debug)] -pub enum ActivationConfirmationError { - #[error("Failed to create activation confirmation directory: {0}")] - CreateConfirmDirError(std::io::Error), - #[error("Failed to create activation confirmation file: {0}")] - CreateConfirmFileError(std::io::Error), - #[error("Failed to create file system watcher instance: {0}")] - CreateWatcherError(notify::Error), - #[error("Error forking process: {0}")] - ForkError(i32), - #[error("Could not watch for activation sentinel: {0}")] - WatcherError(#[from] notify::Error), -} - -#[derive(Error, Debug)] -pub enum DangerZoneError { - #[error("Timeout elapsed for confirmation")] - TimesUp, - #[error("inotify stream ended without activation confirmation")] - NoConfirmation, - #[error("inotify encountered an error: {0}")] - WatchError(notify::Error), -} - -async fn danger_zone( - mut events: mpsc::Receiver>, - confirm_timeout: u16, -) -> Result<(), DangerZoneError> { - info!("Waiting for confirmation event..."); - - match timeout(Duration::from_secs(confirm_timeout as u64), events.recv()).await { - Ok(Some(Ok(()))) => Ok(()), - Ok(Some(Err(e))) => Err(DangerZoneError::WatchError(e)), - Ok(None) => Err(DangerZoneError::NoConfirmation), - Err(_) => Err(DangerZoneError::TimesUp), - } -} - -pub async fn activation_confirmation( - profile_path: String, - temp_path: String, - confirm_timeout: u16, - closure: String, -) -> Result<(), ActivationConfirmationError> { - let lock_path = utils::make_lock_path(&temp_path, &closure); - - debug!("Ensuring parent directory exists for canary file"); - - if let Some(parent) = Path::new(&lock_path).parent() { - fs::create_dir_all(parent) - .await - .map_err(ActivationConfirmationError::CreateConfirmDirError)?; - } - - debug!("Creating canary file"); - - fs::File::create(&lock_path) - .await - .map_err(ActivationConfirmationError::CreateConfirmFileError)?; - - debug!("Creating notify watcher"); - - let (deleted, done) = mpsc::channel(1); - - let mut watcher: RecommendedWatcher = - Watcher::new_immediate(move |res: Result| { - let send_result = match res { - Ok(e) if e.kind == notify::EventKind::Remove(notify::event::RemoveKind::File) => { - debug!("Got worthy removal event, sending on channel"); - deleted.try_send(Ok(())) - } - Err(e) => { - debug!("Got error waiting for removal event, sending on channel"); - deleted.try_send(Err(e)) - } - Ok(_) => Ok(()), // ignore non-removal events - }; - - if let Err(e) = send_result { - error!("Could not send file system event to watcher: {}", e); - } - })?; - - watcher.watch(&lock_path, RecursiveMode::NonRecursive)?; - - if let Err(err) = danger_zone(done, confirm_timeout).await { - error!("Error waiting for confirmation event: {}", err); - - if let Err(err) = deactivate(&profile_path).await { - error!( - "Error de-activating due to another error waiting for confirmation, oh no...: {}", - err - ); - } - } - - Ok(()) -} - -#[derive(Error, Debug)] -pub enum WaitError { - #[error("Error creating watcher for activation: {0}")] - Watcher(#[from] notify::Error), - #[error("Error waiting for activation: {0}")] - Waiting(#[from] DangerZoneError), -} -pub async fn wait(temp_path: String, closure: String) -> Result<(), WaitError> { - let lock_path = utils::make_lock_path(&temp_path, &closure); - - let (created, done) = mpsc::channel(1); - - let mut watcher: RecommendedWatcher = { - // TODO: fix wasteful clone - let lock_path = lock_path.clone(); - - Watcher::new_immediate(move |res: Result| { - let send_result = match res { - Ok(e) if e.kind == notify::EventKind::Create(notify::event::CreateKind::File) => { - match &e.paths[..] { - [x] if x == Path::new(&lock_path) => created.try_send(Ok(())), - _ => Ok(()), - } - } - Err(e) => created.try_send(Err(e)), - Ok(_) => Ok(()), // ignore non-removal events - }; - - if let Err(e) = send_result { - error!("Could not send file system event to watcher: {}", e); - } - })? - }; - - watcher.watch(&temp_path, RecursiveMode::NonRecursive)?; - - // Avoid a potential race condition by checking for existence after watcher creation - if fs::metadata(&lock_path).await.is_ok() { - watcher.unwatch(&temp_path)?; - return Ok(()); - } - - danger_zone(done, 60).await?; - - info!("Found canary file, done waiting!"); - - Ok(()) -} - -#[derive(Error, Debug)] -pub enum ActivateError { - #[error("Failed to execute the command for setting profile: {0}")] - SetProfileError(std::io::Error), - #[error("The command for setting profile resulted in a bad exit code: {0:?}")] - SetProfileExitError(Option), - - #[error("Failed to execute the activation script: {0}")] - RunActivateError(std::io::Error), - #[error("The activation script resulted in a bad exit code: {0:?}")] - RunActivateExitError(Option), - - #[error("There was an error de-activating after an error was encountered: {0}")] - DeactivateError(#[from] DeactivateError), - - #[error("Failed to get activation confirmation: {0}")] - ActivationConfirmationError(#[from] ActivationConfirmationError), -} - -pub async fn activate( - profile_path: String, - closure: String, - auto_rollback: bool, - temp_path: String, - confirm_timeout: u16, - magic_rollback: bool, -) -> Result<(), ActivateError> { - info!("Activating profile"); - - let nix_env_set_exit_status = Command::new("nix-env") - .arg("-p") - .arg(&profile_path) - .arg("--set") - .arg(&closure) - .status() - .await - .map_err(ActivateError::SetProfileError)?; - - match nix_env_set_exit_status.code() { - Some(0) => (), - a => { - if auto_rollback { - deactivate(&profile_path).await?; - } - return Err(ActivateError::SetProfileExitError(a)); - } - }; - - debug!("Running activation script"); - - let activate_status = match Command::new(format!("{}/deploy-rs-activate", profile_path)) - .env("PROFILE", &profile_path) - .current_dir(&profile_path) - .status() - .await - .map_err(ActivateError::RunActivateError) - { - Ok(x) => x, - Err(e) => { - if auto_rollback { - deactivate(&profile_path).await?; - } - return Err(e); - } - }; - - match activate_status.code() { - Some(0) => (), - a => { - if auto_rollback { - deactivate(&profile_path).await?; - } - return Err(ActivateError::RunActivateExitError(a)); - } - }; - - info!("Activation succeeded!"); - - if magic_rollback { - info!("Magic rollback is enabled, setting up confirmation hook..."); - - match activation_confirmation(profile_path.clone(), temp_path, confirm_timeout, closure) - .await - { - Ok(()) => {} - Err(err) => { - deactivate(&profile_path).await?; - return Err(ActivateError::ActivationConfirmationError(err)); - } - }; - } - - Ok(()) -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Ensure that this process stays alive after the SSH connection dies - let mut signals = Signals::new(&[SIGHUP])?; - std::thread::spawn(move || { - for sig in signals.forever() { - println!("Received NOHUP - ignoring..."); - } - }); - - let opts: Opts = Opts::parse(); - - utils::init_logger( - opts.debug_logs, - opts.log_dir.as_deref(), - match opts.subcmd { - SubCommand::Activate(_) => utils::LoggerType::Activate, - SubCommand::Wait(_) => utils::LoggerType::Wait, - }, - )?; - - let r = match opts.subcmd { - SubCommand::Activate(activate_opts) => activate( - activate_opts.profile_path, - activate_opts.closure, - activate_opts.auto_rollback, - opts.temp_path, - activate_opts.confirm_timeout, - activate_opts.magic_rollback, - ) - .await - .map_err(|x| Box::new(x) as Box), - - SubCommand::Wait(wait_opts) => wait(opts.temp_path, wait_opts.closure) - .await - .map_err(|x| Box::new(x) as Box), - }; - - match r { - Ok(()) => (), - Err(err) => good_panic!("{}", err), - } - - Ok(()) -} -- cgit v1.2.3