Skip to content

Commit

Permalink
[#260] Abstract some of the boilerplate
Browse files Browse the repository at this point in the history
Problem: It's common to spawn a command and occasionally fail during
running the command or because it exited, leading to code repetition.

Solution: Create a `command` module to abstract some of the boilerplate
by wrapping `tokio::process::Command`. Create a `run` function that
abstracts most of this boilerplate.

In some parts, the way commands are executed are a bit different, so
instead we leave them as it is.
  • Loading branch information
heitor-lassarote committed Jan 10, 2025
1 parent 92c6681 commit f01f5fc
Show file tree
Hide file tree
Showing 6 changed files with 464 additions and 230 deletions.
154 changes: 89 additions & 65 deletions src/bin/activate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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;

Expand All @@ -24,6 +23,8 @@ use thiserror::Error;

use log::{debug, error, info, warn};

use deploy::command;

/// Remote activation utility for deploy-rs
#[derive(Clap, Debug)]
#[clap(version = "1.0", author = "Serokell <https://serokell.io/>")]
Expand Down Expand Up @@ -121,63 +122,80 @@ struct RevokeOpts {
profile_name: Option<String>,
}

#[derive(Error, Debug)]
pub enum RollbackError {}

impl command::HasCommandError for RollbackError {
fn title() -> String {
"Nix rollback".to_string()
}
}

#[derive(Error, Debug)]
pub enum ListGenError {}

impl command::HasCommandError for ListGenError {
fn title() -> String {
"Nix list generations".to_string()
}
}

#[derive(Error, Debug)]
pub enum DeleteGenError {}

impl command::HasCommandError for DeleteGenError {
fn title() -> String {
"Nix delete generations".to_string()
}
}

#[derive(Error, Debug)]
pub enum ReactivateError {}

impl command::HasCommandError for ReactivateError {
fn title() -> String {
"Nix reactive last generation".to_string()
}
}

#[derive(Error, Debug)]
pub enum DeactivateError {
#[error("Failed to execute the rollback command: {0}")]
Rollback(std::io::Error),
#[error("The rollback resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")]
RollbackExit(Option<i32>, String),
#[error("Failed to run command for listing generations: {0}")]
ListGen(std::io::Error),
#[error("Command for listing generations resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")]
ListGenExit(Option<i32>, String),
#[error("{0}")]
Rollback(#[from] command::CommandError<RollbackError>),
#[error("{0}")]
ListGen(#[from] command::CommandError<ListGenError>),
#[error("Error converting generation list output to utf8: {0}")]
DecodeListGenUtf8(std::string::FromUtf8Error),
#[error("Failed to run command for deleting generation: {0}")]
DeleteGen(std::io::Error),
#[error("Command for deleting generations resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")]
DeleteGenExit(Option<i32>, String),
#[error("Failed to run command for re-activating the last generation: {0}")]
Reactivate(std::io::Error),
#[error("Command for re-activating the last generation resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")]
ReactivateExit(Option<i32>, String),
#[error("{0}")]
DeleteGen(#[from] command::CommandError<DeleteGenError>),
#[error("{0}")]
Reactivate(#[from] command::CommandError<ReactivateError>),
}

pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> {
warn!("De-activating due to error");

let mut nix_env_rollback_command = Command::new("nix-env");
let mut nix_env_rollback_command = command::Command::new("nix-env");
nix_env_rollback_command
.arg("-p")
.arg(&profile_path)
.arg("--rollback");
let nix_env_rollback_exit_status = nix_env_rollback_command
.status()
.arg("--rollback")
.run()
.await
.map_err(DeactivateError::Rollback)?;

match nix_env_rollback_exit_status.code() {
Some(0) => (),
a => return Err(DeactivateError::RollbackExit(a, format!("{:?}", nix_env_rollback_command))),
};

debug!("Listing generations");

let mut nix_env_list_generations_command = Command::new("nix-env");
let mut nix_env_list_generations_command = command::Command::new("nix-env");
nix_env_list_generations_command
.arg("-p")
.arg(&profile_path)
.arg("--list-generations");
let nix_env_list_generations_out = nix_env_list_generations_command
.output()
.run()
.await
.map_err(DeactivateError::ListGen)?;

match nix_env_list_generations_out.status.code() {
Some(0) => (),
a => return Err(DeactivateError::ListGenExit(a, format!("{:?}", nix_env_list_generations_command))),
};

let generations_list = String::from_utf8(nix_env_list_generations_out.stdout)
.map_err(DeactivateError::DecodeListGenUtf8)?;

Expand All @@ -194,38 +212,26 @@ pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> {
debug!("Removing generation entry {}", last_generation_line);
warn!("Removing generation by ID {}", last_generation_id);

let mut nix_env_delete_generation_command = Command::new("nix-env");
let mut nix_env_delete_generation_command = command::Command::new("nix-env");
nix_env_delete_generation_command
.arg("-p")
.arg(&profile_path)
.arg("--delete-generations")
.arg(last_generation_id);
let nix_env_delete_generation_exit_status = nix_env_delete_generation_command
.status()
.arg(last_generation_id)
.run()
.await
.map_err(DeactivateError::DeleteGen)?;

match nix_env_delete_generation_exit_status.code() {
Some(0) => (),
a => return Err(DeactivateError::DeleteGenExit(a, format!("{:?}", nix_env_list_generations_command))),
};

info!("Attempting to re-activate the last generation");

let mut re_activate_command = Command::new(format!("{}/deploy-rs-activate", profile_path));
let mut re_activate_command = command::Command::new(format!("{}/deploy-rs-activate", profile_path));
re_activate_command
.env("PROFILE", &profile_path)
.current_dir(&profile_path);
let re_activate_exit_status = re_activate_command
.status()
.current_dir(&profile_path)
.run()
.await
.map_err(DeactivateError::Reactivate)?;

match re_activate_exit_status.code() {
Some(0) => (),
a => return Err(DeactivateError::ReactivateExit(a,format!("{:?}", re_activate_command))),
};

Ok(())
}

Expand Down Expand Up @@ -370,17 +376,31 @@ pub async fn wait(temp_path: PathBuf, closure: String, activation_timeout: Optio
Ok(())
}

#[derive(Error, Debug)]
pub enum SetProfileError {}

impl command::HasCommandError for SetProfileError {
fn title() -> String {
"Nix profile set".to_string()
}
}

#[derive(Error, Debug)]
pub enum RunActivateError {}

impl command::HasCommandError for RunActivateError {
fn title() -> String {
"Nix activation script".to_string()
}
}

#[derive(Error, Debug)]
pub enum ActivateError {
#[error("Failed to execute the command for setting profile: {0}")]
SetProfile(std::io::Error),
#[error("The command for setting profile resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")]
SetProfileExit(Option<i32>, String),
#[error("{0}")]
SetProfile(#[from] command::CommandError<SetProfileError>),

#[error("Failed to execute the activation script: {0}")]
RunActivate(std::io::Error),
#[error("The activation script resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")]
RunActivateExit(Option<i32>, String),
#[error("{0}")]
RunActivate(#[from] command::CommandError<RunActivateError>),

#[error("There was an error de-activating after an error was encountered: {0}")]
Deactivate(#[from] DeactivateError),
Expand All @@ -401,7 +421,7 @@ pub async fn activate(
) -> Result<(), ActivateError> {
if !dry_activate {
info!("Activating profile");
let mut nix_env_set_command = Command::new("nix-env");
let mut nix_env_set_command = command::Command::new("nix-env");
nix_env_set_command
.arg("-p")
.arg(&profile_path)
Expand All @@ -410,14 +430,16 @@ pub async fn activate(
let nix_env_set_exit_status = nix_env_set_command
.status()
.await
.map_err(ActivateError::SetProfile)?;
.map_err(|err| {
ActivateError::SetProfile(command::CommandError::RunError(err))
})?;
match nix_env_set_exit_status.code() {
Some(0) => (),
a => {
if auto_rollback && !dry_activate {
deactivate(&profile_path).await?;
}
return Err(ActivateError::SetProfileExit(a,format!("{:?}", nix_env_set_command)));
return Err(ActivateError::SetProfile(command::CommandError::Exit(a, format!("{:?}", nix_env_set_command))));
}
};
}
Expand All @@ -430,7 +452,7 @@ pub async fn activate(
&profile_path
};

let mut activate_command = Command::new(format!("{}/deploy-rs-activate", activation_location));
let mut activate_command = command::Command::new(format!("{}/deploy-rs-activate", activation_location));
activate_command
.env("PROFILE", activation_location)
.env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" })
Expand All @@ -439,7 +461,9 @@ pub async fn activate(
let activate_status = match activate_command
.status()
.await
.map_err(ActivateError::RunActivate)
.map_err(|err| {
ActivateError::RunActivate(command::CommandError::RunError(err))
})
{
Ok(x) => x,
Err(e) => {
Expand All @@ -457,7 +481,7 @@ pub async fn activate(
if auto_rollback {
deactivate(&profile_path).await?;
}
return Err(ActivateError::RunActivateExit(a, format!("{:?}", activate_command)));
return Err(ActivateError::RunActivate(command::CommandError::Exit(a, format!("{:?}", activate_command))));
}
};

Expand Down
63 changes: 31 additions & 32 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::io::{stdin, stdout, Write};
use clap::{ArgMatches, Clap, FromArgMatches};

use crate as deploy;
use crate::command;

use self::deploy::{DeployFlake, ParseFlakeError};
use futures_util::stream::{StreamExt, TryStreamExt};
Expand Down Expand Up @@ -124,12 +125,19 @@ async fn test_flake_support() -> Result<bool, std::io::Error> {
.success())
}

#[derive(Error, Debug)]
pub enum NixCheckError {}

impl command::HasCommandError for NixCheckError {
fn title() -> String {
"Nix checking".to_string()
}
}

#[derive(Error, Debug)]
pub enum CheckDeploymentError {
#[error("Failed to execute Nix checking command: {0}")]
NixCheck(#[from] std::io::Error),
#[error("Nix checking command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")]
NixCheckExit(Option<i32>, String),
#[error("{0}")]
NixCheck(#[from] command::CommandError<NixCheckError>),
}

async fn check_deployment(
Expand All @@ -140,8 +148,8 @@ async fn check_deployment(
info!("Running checks for flake in {}", repo);

let mut check_command = match supports_flakes {
true => Command::new("nix"),
false => Command::new("nix-build"),
true => command::Command::new("nix"),
false => command::Command::new("nix-build"),
};

if supports_flakes {
Expand All @@ -154,24 +162,24 @@ async fn check_deployment(

check_command.args(extra_build_args);

let check_status = check_command.status().await?;

match check_status.code() {
Some(0) => (),
a => return Err(CheckDeploymentError::NixCheckExit(a, format!("{:?}", check_command))),
};
check_command.run().await.map_err(CheckDeploymentError::NixCheck)?;

Ok(())
}

#[derive(Error, Debug)]
pub enum NixEvalError {}

impl command::HasCommandError for NixEvalError {
fn title() -> String {
"Nix eval".to_string()
}
}

#[derive(Error, Debug)]
pub enum GetDeploymentDataError {
#[error("Failed to execute nix eval command: {0}")]
NixEval(std::io::Error),
#[error("Failed to read output from evaluation: {0}")]
NixEvalOut(std::io::Error),
#[error("Evaluation resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")]
NixEvalExit(Option<i32>, String),
#[error("{0}")]
NixEval(#[from] command::CommandError<NixEvalError>),
#[error("Error converting evaluation output to utf8: {0}")]
DecodeUtf8(#[from] std::string::FromUtf8Error),
#[error("Error decoding the JSON from evaluation: {0}")]
Expand All @@ -191,9 +199,9 @@ async fn get_deployment_data(
info!("Evaluating flake in {}", flake.repo);

let mut eval_command = if supports_flakes {
Command::new("nix")
command::Command::new("nix")
} else {
Command::new("nix-instantiate")
command::Command::new("nix-instantiate")
};

if supports_flakes {
Expand Down Expand Up @@ -253,20 +261,11 @@ async fn get_deployment_data(

eval_command.args(extra_build_args);

let build_child = eval_command
let build_output = eval_command
.stdout(Stdio::piped())
.spawn()
.map_err(GetDeploymentDataError::NixEval)?;

let build_output = build_child
.wait_with_output()
.run()
.await
.map_err(GetDeploymentDataError::NixEvalOut)?;

match build_output.status.code() {
Some(0) => (),
a => return Err(GetDeploymentDataError::NixEvalExit(a, format!("{:?}", eval_command))),
};
.map_err(GetDeploymentDataError::NixEval)?;

let data_json = String::from_utf8(build_output.stdout)?;

Expand Down
Loading

0 comments on commit f01f5fc

Please sign in to comment.