aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/activate.rs (renamed from src/activate.rs)203
-rw-r--r--src/bin/deploy.rs (renamed from src/main.rs)228
-rw-r--r--src/data.rs (renamed from src/utils/data.rs)0
-rw-r--r--src/deploy.rs297
-rw-r--r--src/lib.rs (renamed from src/utils/mod.rs)146
-rw-r--r--src/push.rs (renamed from src/utils/push.rs)95
-rw-r--r--src/utils/deploy.rs179
7 files changed, 751 insertions, 397 deletions
diff --git a/src/activate.rs b/src/bin/activate.rs
index 84d4b12..2f13b44 100644
--- a/src/activate.rs
+++ b/src/bin/activate.rs
@@ -3,6 +3,8 @@
//
// SPDX-License-Identifier: MPL-2.0
+use signal_hook::{consts::signal::SIGHUP, iterator::Signals};
+
use clap::Clap;
use tokio::fs;
@@ -18,27 +20,44 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use thiserror::Error;
-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
+/// Remote activation utility for deploy-rs
#[derive(Clap, Debug)]
#[clap(version = "1.0", author = "Serokell <https://serokell.io/>")]
struct Opts {
- profile_path: String,
- closure: String,
+ /// Print debug logs to output
+ #[clap(short, long)]
+ debug_logs: bool,
+ /// Directory to print logs to
+ #[clap(long)]
+ log_dir: Option<String>,
- /// Temp path for any temporary files that may be needed during activation
+ /// 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,
@@ -52,6 +71,13 @@ struct Opts {
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}")]
@@ -195,8 +221,9 @@ pub async fn activation_confirmation(
confirm_timeout: u16,
closure: String,
) -> Result<(), ActivationConfirmationError> {
- let lock_hash = &closure["/nix/store/".len()..];
- let lock_path = format!("{}/deploy-rs-canary-{}", temp_path, lock_hash);
+ let lock_path = deploy::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)
@@ -204,53 +231,98 @@ pub async fn activation_confirmation(
.map_err(ActivationConfirmationError::CreateConfirmDirError)?;
}
+ debug!("Creating canary file");
+
fs::File::create(&lock_path)
.await
- .map_err(ActivationConfirmationError::CreateConfirmDirError)?;
+ .map_err(ActivationConfirmationError::CreateConfirmFileError)?;
+
+ debug!("Creating notify watcher");
let (deleted, done) = mpsc::channel(1);
+
let mut watcher: RecommendedWatcher =
Watcher::new_immediate(move |res: Result<notify::event::Event, notify::Error>| {
let send_result = match res {
Ok(e) if e.kind == notify::EventKind::Remove(notify::event::RemoveKind::File) => {
- deleted.blocking_send(Ok(()))
+ 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
- Err(e) => deleted.blocking_send(Err(e)),
};
+
if let Err(e) = send_result {
- // We can't communicate our error, but panic-ing would
- // be bad; let's write an error and trust that the
- // activate function will realize we aren't sending
- // data.
- eprintln!("Could not send file system event to watcher: {}", e);
+ error!("Could not send file system event to watcher: {}", e);
}
})?;
- watcher.watch(lock_path, RecursiveMode::Recursive)?;
- if let fork::Fork::Child =
- fork::daemon(false, false).map_err(ActivationConfirmationError::ForkError)?
- {
- std::thread::spawn(move || {
- let rt = tokio::runtime::Runtime::new().unwrap();
+ watcher.watch(&lock_path, RecursiveMode::NonRecursive)?;
+
+ if let Err(err) = danger_zone(done, confirm_timeout).await {
+ error!("Error waiting for confirmation event: {}", err);
- rt.block_on(async move {
- if let Err(err) = danger_zone(done, confirm_timeout).await {
- if let Err(err) = deactivate(&profile_path).await {
- good_panic!("Error de-activating due to another error in confirmation thread, oh no...: {}", err);
- }
+ if let Err(err) = deactivate(&profile_path).await {
+ error!(
+ "Error de-activating due to another error waiting for confirmation, oh no...: {}",
+ err
+ );
+ }
+ }
- good_panic!("Error in confirmation thread: {}", 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 = deploy::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<notify::event::Event, notify::Error>| {
+ 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(()),
}
- });
- })
- .join()
- .unwrap();
+ }
+ 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)?;
- info!("Confirmation successful!");
+ // 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(());
}
- std::process::exit(0);
+ danger_zone(done, 240).await?;
+
+ info!("Found canary file, done waiting!");
+
+ Ok(())
}
#[derive(Error, Debug)]
@@ -301,6 +373,8 @@ pub async fn activate(
}
};
+ debug!("Running activation script");
+
let activate_status = match Command::new(format!("{}/deploy-rs-activate", profile_path))
.env("PROFILE", &profile_path)
.current_dir(&profile_path)
@@ -331,6 +405,7 @@ pub async fn activate(
if magic_rollback {
info!("Magic rollback is enabled, setting up confirmation hook...");
+
match activation_confirmation(profile_path.clone(), temp_path, confirm_timeout, closure)
.await
{
@@ -347,26 +422,48 @@ pub async fn activate(
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
- if std::env::var("DEPLOY_LOG").is_err() {
- std::env::set_var("DEPLOY_LOG", "info");
- }
-
- pretty_env_logger::init_custom_env("DEPLOY_LOG");
+ // Ensure that this process stays alive after the SSH connection dies
+ let mut signals = Signals::new(&[SIGHUP])?;
+ std::thread::spawn(move || {
+ for _ in signals.forever() {
+ println!("Received NOHUP - ignoring...");
+ }
+ });
let opts: Opts = Opts::parse();
- match activate(
- opts.profile_path,
- opts.closure,
- opts.auto_rollback,
- opts.temp_path,
- opts.confirm_timeout,
- opts.magic_rollback,
- )
- .await
- {
+ deploy::init_logger(
+ opts.debug_logs,
+ opts.log_dir.as_deref(),
+ match opts.subcmd {
+ SubCommand::Activate(_) => deploy::LoggerType::Activate,
+ SubCommand::Wait(_) => deploy::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<dyn std::error::Error>),
+
+ SubCommand::Wait(wait_opts) => wait(opts.temp_path, wait_opts.closure)
+ .await
+ .map_err(|x| Box::new(x) as Box<dyn std::error::Error>),
+ };
+
+ match r {
Ok(()) => (),
- Err(err) => good_panic!("{}", err),
+ Err(err) => {
+ error!("{}", err);
+ std::process::exit(1)
+ }
}
Ok(())
diff --git a/src/main.rs b/src/bin/deploy.rs
index be7ad40..caf3d4e 100644
--- a/src/main.rs
+++ b/src/bin/deploy.rs
@@ -12,17 +12,12 @@ use tokio::process::Command;
use thiserror::Error;
-extern crate pretty_env_logger;
-
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
-#[macro_use]
-mod utils;
-
/// Simple Rust rewrite of a simple Nix Flake deployment tool
#[derive(Clap, Debug)]
#[clap(version = "1.0", author = "Serokell <https://serokell.io/>")]
@@ -39,6 +34,13 @@ struct Opts {
/// Extra arguments to be passed to nix build
extra_build_args: Vec<String>,
+ /// Print debug logs to output
+ #[clap(short, long)]
+ debug_logs: bool,
+ /// Directory to print logs to (including the background activation process)
+ #[clap(long)]
+ log_dir: Option<String>,
+
/// Keep the build outputs of each built profile
#[clap(short, long)]
keep_result: bool,
@@ -98,9 +100,9 @@ async fn test_flake_support() -> Result<bool, std::io::Error> {
#[derive(Error, Debug)]
enum CheckDeploymentError {
#[error("Failed to execute Nix checking command: {0}")]
- NixCheckError(#[from] std::io::Error),
+ NixCheck(#[from] std::io::Error),
#[error("Nix checking command resulted in a bad exit code: {0:?}")]
- NixCheckExitError(Option<i32>),
+ NixCheckExit(Option<i32>),
}
async fn check_deployment(
@@ -136,7 +138,7 @@ async fn check_deployment(
match check_status.code() {
Some(0) => (),
- a => return Err(CheckDeploymentError::NixCheckExitError(a)),
+ a => return Err(CheckDeploymentError::NixCheckExit(a)),
};
Ok(())
@@ -145,15 +147,15 @@ async fn check_deployment(
#[derive(Error, Debug)]
enum GetDeploymentDataError {
#[error("Failed to execute nix eval command: {0}")]
- NixEvalError(std::io::Error),
+ NixEval(std::io::Error),
#[error("Failed to read output from evaluation: {0}")]
- NixEvalOutError(std::io::Error),
+ NixEvalOut(std::io::Error),
#[error("Evaluation resulted in a bad exit code: {0:?}")]
- NixEvalExitError(Option<i32>),
+ NixEvalExit(Option<i32>),
#[error("Error converting evaluation output to utf8: {0}")]
- DecodeUtf8Error(#[from] std::string::FromUtf8Error),
+ DecodeUtf8(#[from] std::string::FromUtf8Error),
#[error("Error decoding the JSON from evaluation: {0}")]
- DecodeJsonError(#[from] serde_json::error::Error),
+ DecodeJson(#[from] serde_json::error::Error),
}
/// Evaluates the Nix in the given `repo` and return the processed Data from it
@@ -161,7 +163,7 @@ async fn get_deployment_data(
supports_flakes: bool,
repo: &str,
extra_build_args: &[String],
-) -> Result<utils::data::Data, GetDeploymentDataError> {
+) -> Result<deploy::data::Data, GetDeploymentDataError> {
info!("Evaluating flake in {}", repo);
let mut c = match supports_flakes {
@@ -193,16 +195,16 @@ async fn get_deployment_data(
let build_child = build_command
.stdout(Stdio::piped())
.spawn()
- .map_err(GetDeploymentDataError::NixEvalError)?;
+ .map_err(GetDeploymentDataError::NixEval)?;
let build_output = build_child
.wait_with_output()
.await
- .map_err(GetDeploymentDataError::NixEvalOutError)?;
+ .map_err(GetDeploymentDataError::NixEvalOut)?;
match build_output.status.code() {
Some(0) => (),
- a => return Err(GetDeploymentDataError::NixEvalExitError(a)),
+ a => return Err(GetDeploymentDataError::NixEvalExit(a)),
};
let data_json = String::from_utf8(build_output.stdout)?;
@@ -220,14 +222,14 @@ struct PromptPart<'a> {
}
fn print_deployment(
- parts: &[(utils::DeployData, utils::DeployDefs)],
+ parts: &[(deploy::DeployData, deploy::DeployDefs)],
) -> Result<(), toml::ser::Error> {
let mut part_map: HashMap<String, HashMap<String, PromptPart>> = HashMap::new();
for (data, defs) in parts {
part_map
.entry(data.node_name.to_string())
- .or_insert(HashMap::new())
+ .or_insert_with(HashMap::new)
.insert(
data.profile_name.to_string(),
PromptPart {
@@ -242,7 +244,7 @@ fn print_deployment(
let toml = toml::to_string(&part_map)?;
- warn!("The following profiles are going to be deployed:\n{}", toml);
+ info!("The following profiles are going to be deployed:\n{}", toml);
Ok(())
}
@@ -259,7 +261,7 @@ enum PromptDeploymentError {
}
fn prompt_deployment(
- parts: &[(utils::DeployData, utils::DeployDefs)],
+ parts: &[(deploy::DeployData, deploy::DeployDefs)],
) -> Result<(), PromptDeploymentError> {
print_deployment(parts)?;
@@ -309,9 +311,9 @@ fn prompt_deployment(
#[derive(Error, Debug)]
enum RunDeployError {
#[error("Failed to deploy profile: {0}")]
- DeployProfileError(#[from] utils::deploy::DeployProfileError),
+ DeployProfile(#[from] deploy::deploy::DeployProfileError),
#[error("Failed to push profile: {0}")]
- PushProfileError(#[from] utils::push::PushProfileError),
+ PushProfile(#[from] deploy::push::PushProfileError),
#[error("No profile named `{0}` was found")]
ProfileNotFound(String),
#[error("No node named `{0}` was found")]
@@ -319,45 +321,78 @@ enum RunDeployError {
#[error("Profile was provided without a node name")]
ProfileWithoutNode,
#[error("Error processing deployment definitions: {0}")]
- DeployDataDefsError(#[from] utils::DeployDataDefsError),
+ DeployDataDefs(#[from] deploy::DeployDataDefsError),
#[error("Failed to make printable TOML of deployment: {0}")]
TomlFormat(#[from] toml::ser::Error),
#[error("{0}")]
- PromptDeploymentError(#[from] PromptDeploymentError),
+ PromptDeployment(#[from] PromptDeploymentError),
}
+type ToDeploy<'a> = Vec<(
+ (&'a str, &'a deploy::data::Node),
+ (&'a str, &'a deploy::data::Profile),
+)>;
+
async fn run_deploy(
- deploy_flake: utils::DeployFlake<'_>,
- data: utils::data::Data,
+ deploy_flake: deploy::DeployFlake<'_>,
+ data: deploy::data::Data,
supports_flakes: bool,
check_sigs: bool,
interactive: bool,
- cmd_overrides: utils::CmdOverrides,
+ cmd_overrides: deploy::CmdOverrides,
keep_result: bool,
result_path: Option<&str>,
extra_build_args: &[String],
+ debug_logs: bool,
+ log_dir: Option<String>,
) -> Result<(), RunDeployError> {
- let to_deploy: Vec<((&str, &utils::data::Node), (&str, &utils::data::Profile))> =
- match (&deploy_flake.node, &deploy_flake.profile) {
- (Some(node_name), Some(profile_name)) => {
- let node = match data.nodes.get(node_name) {
- Some(x) => x,
- None => return Err(RunDeployError::NodeNotFound(node_name.to_owned())),
- };
+ let to_deploy: ToDeploy = match (&deploy_flake.node, &deploy_flake.profile) {
+ (Some(node_name), Some(profile_name)) => {
+ let node = match data.nodes.get(node_name) {
+ Some(x) => x,
+ None => return Err(RunDeployError::NodeNotFound(node_name.to_owned())),
+ };
+ let profile = match node.node_settings.profiles.get(profile_name) {
+ Some(x) => x,
+ None => return Err(RunDeployError::ProfileNotFound(profile_name.to_owned())),
+ };
+
+ vec![((node_name, node), (profile_name, profile))]
+ }
+ (Some(node_name), None) => {
+ let node = match data.nodes.get(node_name) {
+ Some(x) => x,
+ None => return Err(RunDeployError::NodeNotFound(node_name.to_owned())),
+ };
+
+ let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new();
+
+ for profile_name in [
+ node.node_settings.profiles_order.iter().collect(),
+ node.node_settings.profiles.keys().collect::<Vec<&String>>(),
+ ]
+ .concat()
+ {
let profile = match node.node_settings.profiles.get(profile_name) {
Some(x) => x,
None => return Err(RunDeployError::ProfileNotFound(profile_name.to_owned())),
};
- vec![((node_name, node), (profile_name, profile))]
+ if !profiles_list.iter().any(|(n, _)| n == profile_name) {
+ profiles_list.push((&profile_name, profile));
+ }
}
- (Some(node_name), None) => {
- let node = match data.nodes.get(node_name) {
- Some(x) => x,
- None => return Err(RunDeployError::NodeNotFound(node_name.to_owned())),
- };
- let mut profiles_list: Vec<(&str, &utils::data::Profile)> = Vec::new();
+ profiles_list
+ .into_iter()
+ .map(|x| ((node_name.as_str(), node), x))
+ .collect()
+ }
+ (None, None) => {
+ let mut l = Vec::new();
+
+ for (node_name, node) in &data.nodes {
+ let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new();
for profile_name in [
node.node_settings.profiles_order.iter().collect(),
@@ -377,61 +412,31 @@ async fn run_deploy(
}
}
- profiles_list
+ let ll: ToDeploy = profiles_list
.into_iter()
.map(|x| ((node_name.as_str(), node), x))
- .collect()
- }
- (None, None) => {
- let mut l = Vec::new();
-
- for (node_name, node) in &data.nodes {
- let mut profiles_list: Vec<(&str, &utils::data::Profile)> = Vec::new();
-
- for profile_name in [
- node.node_settings.profiles_order.iter().collect(),
- node.node_settings.profiles.keys().collect::<Vec<&String>>(),
- ]
- .concat()
- {
- let profile = match node.node_settings.profiles.get(profile_name) {
- Some(x) => x,
- None => {
- return Err(RunDeployError::ProfileNotFound(
- profile_name.to_owned(),
- ))
- }
- };
-
- if !profiles_list.iter().any(|(n, _)| n == profile_name) {
- profiles_list.push((&profile_name, profile));
- }
- }
-
- let ll: Vec<((&str, &utils::data::Node), (&str, &utils::data::Profile))> =
- profiles_list
- .into_iter()
- .map(|x| ((node_name.as_str(), node), x))
- .collect();
+ .collect();
- l.extend(ll);
- }
-
- l
+ l.extend(ll);
}
- (None, Some(_)) => return Err(RunDeployError::ProfileWithoutNode),
- };
- let mut parts: Vec<(utils::DeployData, utils::DeployDefs)> = Vec::new();
+ l
+ }
+ (None, Some(_)) => return Err(RunDeployError::ProfileWithoutNode),
+ };
+
+ let mut parts: Vec<(deploy::DeployData, deploy::DeployDefs)> = Vec::new();
for ((node_name, node), (profile_name, profile)) in to_deploy {
- let deploy_data = utils::make_deploy_data(
+ let deploy_data = deploy::make_deploy_data(
&data.generic_settings,
node,
node_name,
profile,
profile_name,
&cmd_overrides,
+ debug_logs,
+ log_dir.as_deref(),
);
let deploy_defs = deploy_data.defs()?;
@@ -446,21 +451,21 @@ async fn run_deploy(
}
for (deploy_data, deploy_defs) in &parts {
- utils::push::push_profile(
+ deploy::push::push_profile(deploy::push::PushProfileData {
supports_flakes,
check_sigs,
- deploy_flake.repo,
- &deploy_data,
- &deploy_defs,
+ repo: deploy_flake.repo,
+ deploy_data: &deploy_data,
+ deploy_defs: &deploy_defs,
keep_result,
result_path,
extra_build_args,
- )
+ })
.await?;
}
for (deploy_data, deploy_defs) in &parts {
- utils::deploy::deploy_profile(&deploy_data, &deploy_defs).await?;
+ deploy::deploy::deploy_profile(&deploy_data, &deploy_defs).await?;
}
Ok(())
@@ -469,33 +474,35 @@ async fn run_deploy(
#[derive(Error, Debug)]
enum RunError {
#[error("Failed to deploy profile: {0}")]
- DeployProfileError(#[from] utils::deploy::DeployProfileError),
+ DeployProfile(#[from] deploy::deploy::DeployProfileError),
#[error("Failed to push profile: {0}")]
- PushProfileError(#[from] utils::push::PushProfileError),
+ PushProfile(#[from] deploy::push::PushProfileError),
#[error("Failed to test for flake support: {0}")]
- FlakeTestError(std::io::Error),
+ FlakeTest(std::io::Error),
#[error("Failed to check deployment: {0}")]
- CheckDeploymentError(#[from] CheckDeploymentError),
+ CheckDeployment(#[from] CheckDeploymentError),
#[error("Failed to evaluate deployment data: {0}")]
- GetDeploymentDataError(#[from] GetDeploymentDataError),
+ GetDeploymentData(#[from] GetDeploymentDataError),
#[error("Error parsing flake: {0}")]
- ParseFlakeError(#[from] utils::ParseFlakeError),
+ ParseFlake(#[from] deploy::ParseFlakeError),
+ #[error("Error initiating logger: {0}")]
+ Logger(#[from] flexi_logger::FlexiLoggerError),
#[error("{0}")]
- RunDeployError(#[from] RunDeployError),
+ RunDeploy(#[from] RunDeployError),
}
async fn run() -> Result<(), RunError> {
- 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();
- let deploy_flake = utils::parse_flake(opts.flake.as_str())?;
+ deploy::init_logger(
+ opts.debug_logs,
+ opts.log_dir.as_deref(),
+ deploy::LoggerType::Deploy,
+ )?;
- let cmd_overrides = utils::CmdOverrides {
+ let deploy_flake = deploy::parse_flake(opts.flake.as_str())?;
+
+ let cmd_overrides = deploy::CmdOverrides {
ssh_user: opts.ssh_user,
profile_user: opts.profile_user,
ssh_opts: opts.ssh_opts,
@@ -507,9 +514,7 @@ async fn run() -> Result<(), RunError> {
confirm_timeout: opts.confirm_timeout,
};
- let supports_flakes = test_flake_support()
- .await
- .map_err(RunError::FlakeTestError)?;
+ let supports_flakes = test_flake_support().await.map_err(RunError::FlakeTest)?;
if !supports_flakes {
warn!("A Nix version without flakes support was detected, support for this is work in progress");
@@ -534,6 +539,8 @@ async fn run() -> Result<(), RunError> {
opts.keep_result,
result_path,
&opts.extra_build_args,
+ opts.debug_logs,
+ opts.log_dir,
)
.await?;
@@ -544,7 +551,10 @@ async fn run() -> Result<(), RunError> {
async fn main() -> Result<(), Box<dyn std::error::Error>> {
match run().await {
Ok(()) => (),
- Err(err) => good_panic!("{}", err),
+ Err(err) => {
+ error!("{}", err);
+ std::process::exit(1);
+ }
}
Ok(())
diff --git a/src/utils/data.rs b/src/data.rs
index f557e41..f557e41 100644
--- a/src/utils/data.rs
+++ b/src/data.rs
diff --git a/src/deploy.rs b/src/deploy.rs
new file mode 100644
index 0000000..a33721c
--- /dev/null
+++ b/src/deploy.rs
@@ -0,0 +1,297 @@
+// SPDX-FileCopyrightText: 2020 Serokell <https://serokell.io/>
+// SPDX-FileCopyrightText: 2020 Andreas Fuchs <asf@boinkor.net>
+//
+// SPDX-License-Identifier: MPL-2.0
+
+use std::borrow::Cow;
+use tokio::process::Command;
+
+use thiserror::Error;
+
+struct ActivateCommandData<'a> {
+ sudo: &'a Option<String>,
+ profile_path: &'a str,
+ closure: &'a str,
+ auto_rollback: bool,
+ temp_path: &'a str,
+ confirm_timeout: u16,
+ magic_rollback: bool,
+ debug_logs: bool,
+ log_dir: Option<&'a str>,
+}
+
+fn build_activate_command(data: ActivateCommandData) -> String {
+ let mut self_activate_command = format!("{}/activate-rs", data.closure);
+
+ if data.debug_logs {
+ self_activate_command = format!("{} --debug-logs", self_activate_command);
+ }
+
+ if let Some(log_dir) = data.log_dir {
+ self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir);
+ }
+
+ self_activate_command = format!(
+ "{} --temp-path '{}' activate '{}' '{}'",
+ self_activate_command, data.temp_path, data.closure, data.profile_path
+ );
+
+ self_activate_command = format!(
+ "{} --confirm-timeout {}",
+ self_activate_command, data.confirm_timeout
+ );
+
+ if data.magic_rollback {
+ self_activate_command = format!("{} --magic-rollback", self_activate_command);
+ }
+
+ if data.auto_rollback {
+ self_activate_command = format!("{} --auto-rollback", self_activate_command);
+ }
+
+ if let Some(sudo_cmd) = &data.sudo {
+ self_activate_command = format!("{} {}", sudo_cmd, self_activate_command);
+ }
+
+ self_activate_command
+}
+
+#[test]
+fn test_activation_command_builder() {
+ let sudo = Some("sudo -u test".to_string());
+ let profile_path = "/blah/profiles/test";
+ let closure = "/nix/store/blah/etc";
+ let auto_rollback = true;
+ let temp_path = "/tmp";
+ let confirm_timeout = 30;
+ let magic_rollback = true;
+ let debug_logs = true;
+ let log_dir = Some("/tmp/something.txt");
+
+ assert_eq!(
+ build_activate_command(ActivateCommandData {
+ sudo: &sudo,
+ profile_path,
+ closure,
+ auto_rollback,
+ temp_path,
+ confirm_timeout,
+ magic_rollback,
+ debug_logs,
+ log_dir
+ }),
+ "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt --temp-path '/tmp' activate '/nix/store/blah/etc' '/blah/profiles/test' --confirm-timeout 30 --magic-rollback --auto-rollback"
+ .to_string(),
+ );
+}
+
+struct WaitCommandData<'a> {
+ sudo: &'a Option<String>,
+ closure: &'a str,
+ temp_path: &'a str,
+ debug_logs: bool,
+ log_dir: Option<&'a str>,
+}
+
+fn build_wait_command(data: WaitCommandData) -> String {
+ let mut self_activate_command = format!("{}/activate-rs", data.closure);
+
+ if data.debug_logs {
+ self_activate_command = format!("{} --debug-logs", self_activate_command);
+ }
+
+ if let Some(log_dir) = data.log_dir {
+ self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir);
+ }
+
+ self_activate_command = format!(
+ "{} --temp-path '{}' wait '{}'",
+ self_activate_command, data.temp_path, data.closure
+ );
+
+ if let Some(sudo_cmd) = &data.sudo {
+ self_activate_command = format!("{} {}", sudo_cmd, self_activate_command);
+ }
+
+ self_activate_command
+}
+
+#[test]
+fn test_wait_command_builder() {
+ let sudo = Some("sudo -u test".to_string());
+ let closure = "/nix/store/blah/etc";
+ let temp_path = "/tmp";
+ let debug_logs = true;
+ let log_dir = Some("/tmp/something.txt");
+
+ assert_eq!(
+ build_wait_command(WaitCommandData {
+ sudo: &sudo,
+ closure,
+ temp_path,
+ debug_logs,
+ log_dir
+ }),
+ "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt --temp-path '/tmp' wait '/nix/store/blah/etc'"
+ .to_string(),
+ );
+}
+
+#[derive(Error, Debug)]
+pub enum DeployProfileError {
+ #[error("Failed to spawn activation command over SSH: {0}")]
+ SSHSpawnActivateError(std::io::Error),
+
+ #[error("Failed to run activation command over SSH: {0}")]
+ SSHActivateError(std::io::Error),
+ #[error("Activating over SSH resulted in a bad exit code: {0:?}")]
+ SSHActivateExitError(Option<i32>),
+
+ #[error("Failed to run wait command over SSH: {0}")]
+ SSHWaitError(std::io::Error),
+ #[error("Waiting over SSH resulted in a bad exit code: {0:?}")]
+ SSHWaitExitError(Option<i32>),
+
+ #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")]
+ SSHConfirmError(std::io::Error),
+ #[error(
+ "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}"
+ )]
+ SSHConfirmExitError(Option<i32>),
+}
+
+pub async fn deploy_profile(
+ deploy_data: &super::DeployData<'_>,
+ deploy_defs: &super::DeployDefs,
+) -> Result<(), DeployProfileError> {
+ info!(
+ "Activating profile `{}` for node `{}`",
+ deploy_data.profile_name, deploy_data.node_name
+ );
+
+ let temp_path: Cow<str> = match &deploy_data.merged_settings.temp_path {
+ Some(x) => x.into(),
+ None => "/tmp".into(),
+ };
+
+ let confirm_timeout = deploy_data.merged_settings.confirm_timeout.unwrap_or(30);
+
+ let magic_rollback = deploy_data.merged_settings.magic_rollback.unwrap_or(true);
+
+ let auto_rollback = deploy_data.merged_settings.auto_rollback.unwrap_or(true);
+
+ let self_activate_command = build_activate_command(ActivateCommandData {
+ sudo: &deploy_defs.sudo,
+ profile_path: &deploy_defs.profile_path,
+ closure: &deploy_data.profile.profile_settings.path,
+ auto_rollback,
+ temp_path: &temp_path,
+ confirm_timeout,
+ magic_rollback,
+ debug_logs: deploy_data.debug_logs,
+ log_dir: deploy_data.log_dir,
+ });
+
+ debug!("Constructed activation command: {}", self_activate_command);
+
+ let hostname = match deploy_data.cmd_overrides.hostname {
+ Some(ref x) => x,
+ None => &deploy_data.node.node_settings.hostname,
+ };
+
+ let ssh_addr = format!("ssh://{}@{}", deploy_defs.ssh_user, hostname);
+
+ let mut ssh_activate_command_ = Command::new("ssh");
+ let ssh_activate_command = ssh_activate_command_.arg(&ssh_addr);
+
+ for ssh_opt in &deploy_data.merged_settings.ssh_opts {
+ ssh_activate_command.arg(&ssh_opt);
+ }
+
+ if !magic_rollback {
+ let ssh_activate_exit_status = ssh_activate_command
+ .arg(self_activate_command)
+ .status()
+ .await
+ .map_err(DeployProfileError::SSHActivateError)?;
+
+ match ssh_activate_exit_status.code() {
+ Some(0) => (),
+ a => return Err(DeployProfileError::SSHActivateExitError(a)),
+ };
+
+ info!("Success activating, done!");
+ } else {
+ let self_wait_command = build_wait_command(WaitCommandData {
+ sudo: &deploy_defs.sudo,
+ closure: &deploy_data.profile.profile_settings.path,
+ temp_path: &temp_path,
+ debug_logs: deploy_data.debug_logs,
+ log_dir: deploy_data.log_dir,
+ });
+
+ debug!("Constructed wait command: {}", self_wait_command);
+
+ let ssh_activate = ssh_activate_command
+ .arg(self_activate_command)
+ .spawn()
+ .map_err(DeployProfileError::SSHSpawnActivateError)?;
+
+ info!("Creating activation waiter");
+
+ let mut ssh_wait_command_ = Command::new("ssh");
+ let ssh_wait_command = ssh_wait_command_.arg(&ssh_addr);
+
+ for ssh_opt in &deploy_data.merged_settings.ssh_opts {
+ ssh_wait_command.arg(ssh_opt);
+ }
+
+ let ssh_wait_exit_status = ssh_wait_command
+ .arg(self_wait_command)
+ .status()
+ .await
+ .map_err(DeployProfileError::SSHWaitError)?;
+
+ match ssh_wait_exit_status.code() {
+ Some(0) => (),
+ a => return Err(DeployProfileError::SSHWaitExitError(a)),
+ };
+
+ info!("Success activating, attempting to confirm activation");
+
+ let mut c = Command::new("ssh");
+ let mut ssh_confirm_command = c.arg(format!("ssh://{}@{}", deploy_defs.ssh_user, hostname));
+
+ for ssh_opt in &deploy_data.merged_settings.ssh_opts {
+ ssh_confirm_command = ssh_confirm_command.arg(ssh_opt);
+ }
+
+ let lock_path =
+ super::make_lock_path(&temp_path, &deploy_data.profile.profile_settings.path);
+
+ let mut confirm_command = format!("rm {}", lock_path);
+ if let Some(sudo_cmd) = &deploy_defs.sudo {
+ confirm_command = format!("{} {}", sudo_cmd, confirm_command);
+ }
+
+ debug!(
+ "Attempting to run command to confirm deployment: {}",
+ confirm_command
+ );
+
+ let ssh_exit_status = ssh_confirm_command
+ .arg(confirm_command)
+ .status()
+ .await
+ .map_err(DeployProfileError::SSHConfirmError)?;
+
+ match ssh_exit_status.code() {
+ Some(0) => (),
+ a => return Err(DeployProfileError::SSHConfirmExitError(a)),
+ };
+
+ info!("Deployment confirmed.");
+ }
+
+ Ok(())
+}
diff --git a/src/utils/mod.rs b/src/lib.rs
index a891261..edc0507 100644
--- a/src/utils/mod.rs
+++ b/src/lib.rs
@@ -3,20 +3,132 @@
//
// SPDX-License-Identifier: MPL-2.0
-use rnix::{types::*, NodeOrToken, SyntaxKind::*, SyntaxNode};
-
-use std::path::PathBuf;
+use rnix::{types::*, SyntaxKind::*};
use merge::Merge;
use thiserror::Error;
-#[macro_export]
-macro_rules! good_panic {
- ($($tts:tt)*) => {{
- error!($($tts)*);
- std::process::exit(1);
- }}
+use flexi_logger::*;
+
+#[macro_use]
+extern crate log;
+
+#[macro_use]
+extern crate serde_derive;
+
+pub fn make_lock_path(temp_path: &str, closure: &str) -> String {
+ let lock_hash =
+ &closure["/nix/store/".len()..closure.find('-').unwrap_or_else(|| closure.len())];
+ format!("{}/deploy-rs-canary-{}", temp_path, lock_hash)
+}
+
+fn make_emoji(level: log::Level) -> &'static str {
+ match level {
+ log::Level::Error => "❌",
+ log::Level::Warn => "⚠️",
+ log::Level::Info => "ℹ️",
+ log::Level::Debug => "❓",
+ log::Level::Trace => "🖊️",
+ }
+}
+
+pub fn logger_formatter_activate(
+ w: &mut dyn std::io::Write,
+ _now: &mut DeferredNow,
+ record: &Record,
+) -> Result<(), std::io::Error> {
+ let level = record.level();
+
+ write!(
+ w,
+ "⭐ {} [activate] [{}] {}",
+ make_emoji(level),
+ style(level, level.to_string()),
+ record.args()
+ )
+}
+
+pub fn logger_formatter_wait(
+ w: &mut dyn std::io::Write,
+ _now: &mut DeferredNow,
+ record: &Record,
+) -> Result<(), std::io::Error> {
+ let level = record.level();
+
+ write!(
+ w,
+ "👀 {} [wait] [{}] {}",
+ make_emoji(level),
+ style(level, level.to_string()),
+ record.args()
+ )
+}
+
+pub fn logger_formatter_deploy(
+ w: &mut dyn std::io::Write,
+ _now: &mut DeferredNow,
+ record: &Record,
+) -> Result<(), std::io::Error> {
+ let level = record.level();
+
+ write!(
+ w,
+ "🚀 {} [deploy] [{}] {}",
+ make_emoji(level),
+ style(level, level.to_string()),
+ record.args()
+ )
+}
+
+pub enum LoggerType {
+ Deploy,
+ Activate,
+ Wait,
+}
+
+pub fn init_logger(
+ debug_logs: bool,
+ log_dir: Option<&str>,
+ logger_type: LoggerType,
+) -> Result<(), FlexiLoggerError> {
+ let logger_formatter = match logger_type {
+ LoggerType::Deploy => logger_formatter_deploy,
+ LoggerType::Activate => logger_formatter_activate,
+ LoggerType::Wait => logger_formatter_wait,
+ };
+
+ if let Some(log_dir) = log_dir {
+ let mut logger = Logger::with_env_or_str("debug")
+ .log_to_file()
+ .format_for_stderr(logger_formatter)
+ .set_palette("196;208;51;7;8".to_string())
+ .directory(log_dir)
+ .duplicate_to_stderr(match debug_logs {
+ true => Duplicate::Debug,
+ false => Duplicate::Info,
+ })
+ .print_message();
+
+ match logger_type {
+ LoggerType::Activate => logger = logger.discriminant("activate"),
+ LoggerType::Wait => logger = logger.discriminant("wait"),
+ LoggerType::Deploy => (),
+ }
+
+ logger.start()?;
+ } else {
+ Logger::with_env_or_str(match debug_logs {
+ true => "debug",
+ false => "info",
+ })
+ .log_target(LogTarget::StdErr)
+ .format(logger_formatter)
+ .set_palette("196;208;51;7;8".to_string())
+ .start()?;
+ }
+
+ Ok(())
}
pub mod data;
@@ -191,6 +303,9 @@ pub struct DeployData<'a> {
pub cmd_overrides: &'a CmdOverrides,
pub merged_settings: data::GenericSettings,
+
+ pub debug_logs: bool,
+ pub log_dir: Option<&'a str>,
}
#[derive(Debug)]
@@ -259,6 +374,8 @@ pub fn make_deploy_data<'a, 's>(
profile: &'a data::Profile,
profile_name: &'a str,
cmd_overrides: &'a CmdOverrides,
+ debug_logs: bool,
+ log_dir: Option<&'a str>,
) -> DeployData<'a> {
let mut merged_settings = profile.generic_settings.clone();
merged_settings.merge(node.generic_settings.clone());
@@ -292,13 +409,8 @@ pub fn make_deploy_data<'a, 's>(
cmd_overrides,
merged_settings,
- }
-}
-#[derive(Error, Debug)]
-pub enum DeployPathToActivatePathError {
- #[error("Deploy path did not have a parent directory")]
- PathTooShort,
- #[error("Deploy path was not valid utf8")]
- InvalidUtf8,
+ debug_logs,
+ log_dir,
+ }
}
diff --git a/src/utils/push.rs b/src/push.rs
index 503e062..2f83019 100644
--- a/src/utils/push.rs
+++ b/src/push.rs
@@ -2,22 +2,22 @@
//
// SPDX-License-Identifier: MPL-2.0
+use std::path::Path;
use std::process::Stdio;
use tokio::process::Command;
-use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PushProfileError {
- #[error("Failed to calculate activate bin path from deploy bin path: {0}")]
- DeployPathToActivatePathError(#[from] super::DeployPathToActivatePathError),
#[error("Failed to run Nix build command: {0}")]
BuildError(std::io::Error),
#[error("Nix build command resulted in a bad exit code: {0:?}")]
BuildExitError(Option<i32>),
- #[error("Activation script deploy-rs-activate does not exist in profile.\n\
- Did you forget to use deploy-rs#lib.<...>.activate.<...> on your profile path?")]
+ #[error(
+ "Activation script deploy-rs-activate does not exist in profile.\n\
+ Did you forget to use deploy-rs#lib.<...>.activate.<...> on your profile path?"
+ )]
DeployRsActivateDoesntExist,
#[error("Activation script activate-rs does not exist in profile.\n\
Is there a mismatch in deploy-rs used in the flake you're deploying and deploy-rs command you're running?")]
@@ -32,53 +32,55 @@ pub enum PushProfileError {
CopyExitError(Option<i32>),
}
-pub async fn push_profile(
- supports_flakes: bool,
- check_sigs: bool,
- repo: &str,
- deploy_data: &super::DeployData<'_>,
- deploy_defs: &super::DeployDefs,
- keep_result: bool,
- result_path: Option<&str>,
- extra_build_args: &[String],
-) -> Result<(), PushProfileError> {
+pub struct PushProfileData<'a> {
+ pub supports_flakes: bool,
+ pub check_sigs: bool,
+ pub repo: &'a str,
+ pub deploy_data: &'a super::DeployData<'a>,
+ pub deploy_defs: &'a super::DeployDefs,
+ pub keep_result: bool,
+ pub result_path: Option<&'a str>,
+ pub extra_build_args: &'a [String],
+}
+
+pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileError> {
info!(
"Building profile `{}` for node `{}`",
- deploy_data.profile_name, deploy_data.node_name
+ data.deploy_data.profile_name, data.deploy_data.node_name
);
- let mut build_c = if supports_flakes {
+ let mut build_c = if data.supports_flakes {
Command::new("nix")
} else {
Command::new("nix-build")
};
- let mut build_command = if supports_flakes {
+ let mut build_command = if data.supports_flakes {
build_c.arg("build").arg(format!(
"{}#deploy.nodes.\"{}\".profiles.\"{}\".path",
- repo, deploy_data.node_name, deploy_data.profile_name
+ data.repo, data.deploy_data.node_name, data.deploy_data.profile_name
))
} else {
- build_c.arg(&repo).arg("-A").arg(format!(
+ build_c.arg(&data.repo).arg("-A").arg(format!(
"deploy.nodes.\"{}\".profiles.\"{}\".path",
- deploy_data.node_name, deploy_data.profile_name
+ data.deploy_data.node_name, data.deploy_data.profile_name
))
};
- build_command = match (keep_result, supports_flakes) {
+ build_command = match (data.keep_result, data.supports_flakes) {
(true, _) => {
- let result_path = result_path.unwrap_or("./.deploy-gc");
+ let result_path = data.result_path.unwrap_or("./.deploy-gc");
build_command.arg("--out-link").arg(format!(
"{}/{}/{}",
- result_path, deploy_data.node_name, deploy_data.profile_name
+ result_path, data.deploy_data.node_name, data.deploy_data.profile_name
))
}
(false, false) => build_command.arg("--no-out-link"),
(false, true) => build_command.arg("--no-link"),
};
- for extra_arg in extra_build_args {
+ for extra_arg in data.extra_build_args {
build_command = build_command.arg(extra_arg);
}
@@ -94,20 +96,34 @@ pub async fn push_profile(
a => return Err(PushProfileError::BuildExitError(a)),
};
- if ! Path::new(format!("{}/deploy-rs-activate", deploy_data.profile.profile_settings.path).as_str()).exists() {
+ if !Path::new(
+ format!(
+ "{}/deploy-rs-activate",
+ data.deploy_data.profile.profile_settings.path
+ )
+ .as_str(),
+ )
+ .exists()
+ {
return Err(PushProfileError::DeployRsActivateDoesntExist);
}
- if ! Path::new(format!("{}/activate-rs", deploy_data.profile.profile_settings.path).as_str()).exists() {
+ if !Path::new(
+ format!(
+ "{}/activate-rs",
+ data.deploy_data.profile.profile_settings.path
+ )
+ .as_str(),
+ )
+ .exists()
+ {
return Err(PushProfileError::ActivateRsDoesntExist);
}
-
-
if let Ok(local_key) = std::env::var("LOCAL_KEY") {
info!(
"Signing key present! Signing profile `{}` for node `{}`",
- deploy_data.profile_name, deploy_data.node_name
+ data.deploy_data.profile_name, data.deploy_data.node_name
);
let sign_exit_status = Command::new("nix")
@@ -115,7 +131,7 @@ pub async fn push_profile(
.arg("-r")
.arg("-k")
.arg(local_key)
- .arg(&deploy_data.profile.profile_settings.path)
+ .arg(&data.deploy_data.profile.profile_settings.path)
.status()
.await
.map_err(PushProfileError::SignError)?;
@@ -128,21 +144,22 @@ pub async fn push_profile(
debug!(
"Copying profile `{}` to node `{}`",
- deploy_data.profile_name, deploy_data.node_name
+ data.deploy_data.profile_name, data.deploy_data.node_name
);
let mut copy_command_ = Command::new("nix");
let mut copy_command = copy_command_.arg("copy");
- if deploy_data.merged_settings.fast_connection != Some(true) {
+ if data.deploy_data.merged_settings.fast_connection != Some(true) {
copy_command = copy_command.arg("--substitute-on-destination");
}
- if !check_sigs {
+ if !data.check_sigs {
copy_command = copy_command.arg("--no-check-sigs");
}
- let ssh_opts_str = deploy_data
+ let ssh_opts_str = data
+ .deploy_data
.merged_settings
.ssh_opts
// This should provide some extra safety, but it also breaks for some reason, oh well
@@ -151,15 +168,15 @@ pub async fn push_profile(
// .collect::<Vec<String>>()
.join(" ");
- let hostname = match deploy_data.cmd_overrides.hostname {
+ let hostname = match data.deploy_data.cmd_overrides.hostname {
Some(ref x) => x,
- None => &deploy_data.node.node_settings.hostname,
+ None => &data.deploy_data.node.node_settings.hostname,
};
let copy_exit_status = copy_command
.arg("--to")
- .arg(format!("ssh://{}@{}", deploy_defs.ssh_user, hostname))
- .arg(&deploy_data.profile.profile_settings.path)
+ .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname))
+ .arg(&data.deploy_data.profile.profile_settings.path)
.env("NIX_SSHOPTS", ssh_opts_str)
.status()
.await
diff --git a/src/utils/deploy.rs b/src/utils/deploy.rs
deleted file mode 100644
index 14a44a0..0000000
--- a/src/utils/deploy.rs
+++ /dev/null
@@ -1,179 +0,0 @@
-// SPDX-FileCopyrightText: 2020 Serokell <https://serokell.io/>
-// SPDX-FileCopyrightText: 2020 Andreas Fuchs <asf@boinkor.net>
-//
-// SPDX-License-Identifier: MPL-2.0
-
-use std::borrow::Cow;
-use tokio::process::Command;
-
-use thiserror::Error;
-
-fn build_activate_command(
- sudo: &Option<String>,
- profile_path: &str,
- closure: &str,
- auto_rollback: bool,
- temp_path: &Cow<str>,
- confirm_timeout: u16,
- magic_rollback: bool,
-) -> String {
- let mut self_activate_command = format!(
- "{}/activate-rs '{}' '{}' --temp-path {} --confirm-timeout {}",
- closure, profile_path, closure, temp_path, confirm_timeout
- );
-
- if magic_rollback {
- self_activate_command = format!("{} --magic-rollback", self_activate_command);
- }
-
- if auto_rollback {
- self_activate_command = format!("{} --auto-rollback", self_activate_command);
- }
-
- if let Some(sudo_cmd) = &sudo {
- self_activate_command = format!("{} {}", sudo_cmd, self_activate_command);
- }
-
- self_activate_command
-}
-
-#[test]
-fn test_activation_command_builder() {
- let activate_path_str = "/blah/bin/activate".to_string();
- let sudo = Some("sudo -u test".to_string());
- let profile_path = "/blah/profiles/test";
- let closure = "/nix/store/blah/etc";
- let auto_rollback = true;
- let temp_path = &"/tmp".into();
- let confirm_timeout = 30;
- let magic_rollback = true;
-
- assert_eq!(
- build_activate_command(
- &sudo,
- profile_path,
- closure,
- auto_rollback,
- temp_path,
- confirm_timeout,
- magic_rollback
- ),
- "sudo -u test /nix/store/blah/etc/activate-rs '/blah/profiles/test' '/nix/store/blah/etc' --temp-path /tmp --confirm-timeout 30 --magic-rollback --auto-rollback"
- .to_string(),
- );
-}
-
-#[derive(Error, Debug)]
-pub enum DeployProfileError {
- #[error("Failed to calculate activate bin path from deploy bin path: {0}")]
- DeployPathToActivatePathError(#[from] super::DeployPathToActivatePathError),
- #[error("Failed to run activation command over SSH: {0}")]
- SSHActivateError(std::io::Error),
- #[error("Activation over SSH resulted in a bad exit code: {0:?}")]
- SSHActivateExitError(Option<i32>),
- #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")]
- SSHConfirmError(std::io::Error),
- #[error(
- "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}"
- )]
- SSHConfirmExitError(Option<i32>),
-}
-
-pub async fn deploy_profile(
- deploy_data: &super::DeployData<'_>,
- deploy_defs: &super::DeployDefs,
-) -> Result<(), DeployProfileError> {
- info!(
- "Activating profile `{}` for node `{}`",
- deploy_data.profile_name, deploy_data.node_name
- );
-
- let temp_path: Cow<str> = match &deploy_data.merged_settings.temp_path {
- Some(x) => x.into(),
- None => "/tmp".into(),
- };
-
- let confirm_timeout = deploy_data.merged_settings.confirm_timeout.unwrap_or(30);
-
- let magic_rollback = deploy_data.merged_settings.magic_rollback.unwrap_or(true);
-
- let auto_rollback = deploy_data.merged_settings.auto_rollback.unwrap_or(true);
-
- let self_activate_command = build_activate_command(
- &deploy_defs.sudo,
- &deploy_defs.profile_path,
- &deploy_data.profile.profile_settings.path,
- auto_rollback,
- &temp_path,
- confirm_timeout,
- magic_rollback,
- );
-
- debug!("Constructed activation command: {}", self_activate_command);
-
- let hostname = match deploy_data.cmd_overrides.hostname {
- Some(ref x) => x,
- None => &deploy_data.node.node_settings.hostname,
- };
-
- let mut c = Command::new("ssh");
- let mut ssh_command = c
- .arg("-t")
- .arg(format!("ssh://{}@{}", deploy_defs.ssh_user, hostname));
-
- for ssh_opt in &deploy_data.merged_settings.ssh_opts {
- ssh_command = ssh_command.arg(ssh_opt);
- }
-
- let ssh_exit_status = ssh_command
- .arg(self_activate_command)
- .status()
- .await
- .map_err(DeployProfileError::SSHActivateError)?;
-
- match ssh_exit_status.code() {
- Some(0) => (),
- a => return Err(DeployProfileError::SSHActivateExitError(a)),
- };
-
- info!("Success activating!");
-
- if magic_rollback {
- info!("Attempting to confirm activation");
-
- let mut c = Command::new("ssh");
- let mut ssh_confirm_command = c.arg(format!("ssh://{}@{}", deploy_defs.ssh_user, hostname));
-
- for ssh_opt in &deploy_data.merged_settings.ssh_opts {
- ssh_confirm_command = ssh_confirm_command.arg(ssh_opt);
- }
-
- let lock_hash = &deploy_data.profile.profile_settings.path["/nix/store/".len()..];
- let lock_path = format!("{}/deploy-rs-canary-{}", temp_path, lock_hash);
-
- let mut confirm_command = format!("rm {}", lock_path);
- if let Some(sudo_cmd) = &deploy_defs.sudo {
- confirm_command = format!("{} {}", sudo_cmd, confirm_command);
- }
-
- debug!(
- "Attempting to run command to confirm deployment: {}",
- confirm_command
- );
-
- let ssh_exit_status = ssh_confirm_command
- .arg(confirm_command)
- .status()
- .await
- .map_err(DeployProfileError::SSHConfirmError)?;
-
- match ssh_exit_status.code() {
- Some(0) => (),
- a => return Err(DeployProfileError::SSHConfirmExitError(a)),
- };
-
- info!("Deployment confirmed.");
- }
-
- Ok(())
-}