// SPDX-FileCopyrightText: 2020 Serokell
//
// SPDX-License-Identifier: MPL-2.0
use clap::Clap;
use futures_util::FutureExt;
use std::process::Stdio;
use tokio::fs;
use tokio::process::Command;
use tokio::time::timeout;
use std::time::Duration;
use futures_util::StreamExt;
use std::path::Path;
use inotify::Inotify;
extern crate pretty_env_logger;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
#[macro_use]
mod utils;
/// Activation portion of the simple Rust Nix deploy tool
#[derive(Clap, Debug)]
#[clap(version = "1.0", author = "notgne2 ")]
struct Opts {
profile_path: String,
closure: String,
/// Temp path for any temporary files that may be needed during activation
#[clap(long)]
temp_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,
/// Command for bootstrapping
#[clap(long)]
bootstrap_cmd: Option,
/// Auto rollback if failure
#[clap(long)]
auto_rollback: bool,
}
pub async fn deactivate(profile_path: &str) -> Result<(), Box> {
error!("De-activating due to error");
let nix_env_rollback_exit_status = Command::new("nix-env")
.arg("-p")
.arg(&profile_path)
.arg("--rollback")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await?;
if !nix_env_rollback_exit_status.success() {
good_panic!("`nix-env --rollback` failed");
}
debug!("Listing generations");
let nix_env_list_generations_out = Command::new("nix-env")
.arg("-p")
.arg(&profile_path)
.arg("--list-generations")
.output()
.await?;
if !nix_env_list_generations_out.status.success() {
good_panic!("Listing `nix-env` generations failed");
}
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)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await?;
if !nix_env_delete_generation_exit_status.success() {
good_panic!("Failed to delete failed generation");
}
info!("Attempting re-activate 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?;
if !re_activate_exit_status.success() {
good_panic!("Failed to re-activate the last generation");
}
Ok(())
}
async fn deactivate_on_err(profile_path: &str, r: Result) -> A {
match r {
Ok(x) => x,
Err(err) => {
error!("Deactivating due to error: {:?}", err);
match deactivate(profile_path).await {
Ok(_) => (),
Err(err) => {
error!("Error de-activating, uh-oh: {:?}", err);
}
};
std::process::exit(1);
}
}
}
pub async fn activation_confirmation(
profile_path: String,
temp_path: String,
confirm_timeout: u16,
closure: String,
) -> Result<(), Box> {
let lock_hash = &closure[11 /* /nix/store/ */ ..];
let lock_path = format!("{}/activating-{}", temp_path, lock_hash);
if let Some(parent) = Path::new(&lock_path).parent() {
fs::create_dir_all(parent).await?;
}
fs::File::create(&lock_path).await?;
let mut inotify = Inotify::init()?;
inotify.add_watch(lock_path, inotify::WatchMask::DELETE)?;
match fork::daemon(false, false).map_err(|x| x.to_string())? {
fork::Fork::Child => {
std::thread::spawn(move || {
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
info!("Waiting for confirmation event...");
let mut buffer = [0; 32];
let mut stream =
deactivate_on_err(&profile_path, inotify.event_stream(&mut buffer)).await;
deactivate_on_err(
&profile_path,
deactivate_on_err(
&profile_path,
deactivate_on_err(
&profile_path,
timeout(Duration::from_secs(confirm_timeout as u64), stream.next())
.await,
)
.await
.ok_or("Watcher ended prematurely"),
)
.await,
)
.await;
});
})
.join()
.unwrap();
info!("Confirmation successful!");
std::process::exit(0);
}
fork::Fork::Parent(_) => {
std::process::exit(0);
}
}
}
pub async fn activate(
profile_path: String,
closure: String,
bootstrap_cmd: Option,
auto_rollback: bool,
temp_path: String,
confirm_timeout: u16,
magic_rollback: bool,
) -> Result<(), Box> {
info!("Activating profile");
let nix_env_set_exit_status = Command::new("nix-env")
.arg("-p")
.arg(&profile_path)
.arg("--set")
.arg(&closure)
.stdout(Stdio::null())
.status()
.await?;
if !nix_env_set_exit_status.success() {
good_panic!("Failed to update nix-env generation");
}
if let (Some(bootstrap_cmd), false) = (bootstrap_cmd, !Path::new(&profile_path).exists()) {
let bootstrap_status = Command::new("bash")
.arg("-c")
.arg(&bootstrap_cmd)
.env("PROFILE", &profile_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await;
match bootstrap_status {
Ok(s) if s.success() => (),
_ => {
tokio::fs::remove_file(&profile_path).await?;
good_panic!("Failed to execute bootstrap command");
}
}
}
let activate_status = Command::new(format!("{}/deploy-rs-activate", profile_path))
.env("PROFILE", &profile_path)
.current_dir(&profile_path)
.status()
.await;
match activate_status {
Ok(s) if s.success() => (),
_ if auto_rollback => return Ok(deactivate(&profile_path).await?),
_ => (),
}
info!("Activation succeeded!");
if magic_rollback {
info!("Performing activation confirmation steps");
deactivate_on_err(
&profile_path,
activation_confirmation(profile_path.clone(), temp_path, confirm_timeout, closure)
.await,
)
.await;
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box> {
if std::env::var("DEPLOY_LOG").is_err() {
std::env::set_var("DEPLOY_LOG", "info");
}
pretty_env_logger::init_custom_env("DEPLOY_LOG");
let opts: Opts = Opts::parse();
activate(
opts.profile_path,
opts.closure,
opts.bootstrap_cmd,
opts.auto_rollback,
opts.temp_path,
opts.confirm_timeout,
opts.magic_rollback,
)
.await?;
Ok(())
}