diff options
Diffstat (limited to '')
-rw-r--r-- | src/utils/data.rs | 64 | ||||
-rw-r--r-- | src/utils/deploy.rs | 57 | ||||
-rw-r--r-- | src/utils/mod.rs | 71 | ||||
-rw-r--r-- | src/utils/push.rs | 108 |
4 files changed, 300 insertions, 0 deletions
diff --git a/src/utils/data.rs b/src/utils/data.rs new file mode 100644 index 0000000..779d913 --- /dev/null +++ b/src/utils/data.rs @@ -0,0 +1,64 @@ +use merge::Merge; + +use std::{collections::HashMap}; + +#[derive(Deserialize, Debug, Clone, Merge)] +pub struct GenericSettings { + #[serde(rename(deserialize = "sshUser"))] + pub ssh_user: Option<String>, + pub user: Option<String>, + #[serde( + skip_serializing_if = "Vec::is_empty", + default, + rename(deserialize = "sshOpts") + )] + #[merge(strategy = merge::vec::append)] + pub ssh_opts: Vec<String>, + #[serde(rename(deserialize = "fastConnection"))] + pub fast_connection: Option<bool>, + #[serde(rename(deserialize = "autoRollback"))] + pub auto_rollback: Option<String>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct NodeSettings { + pub hostname: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ProfileSettings { + pub path: String, + pub activate: Option<String>, + pub bootstrap: Option<String>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Profile { + #[serde(flatten)] + pub profile_settings: ProfileSettings, + #[serde(flatten)] + pub generic_settings: GenericSettings, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Node { + #[serde(flatten)] + pub generic_settings: GenericSettings, + #[serde(flatten)] + pub node_settings: NodeSettings, + + pub profiles: HashMap<String, Profile>, + #[serde( + skip_serializing_if = "Vec::is_empty", + default, + rename(deserialize = "profilesOrder") + )] + pub profiles_order: Vec<String>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Data { + #[serde(flatten)] + pub generic_settings: GenericSettings, + pub nodes: HashMap<String, Node>, +} diff --git a/src/utils/deploy.rs b/src/utils/deploy.rs new file mode 100644 index 0000000..7260e55 --- /dev/null +++ b/src/utils/deploy.rs @@ -0,0 +1,57 @@ +use super::data; + + +use tokio::process::Command; + +pub async fn deploy_profile( + profile: &data::Profile, + profile_name: &str, + node: &data::Node, + node_name: &str, + merged_settings: &data::GenericSettings, + deploy_data: &super::DeployData<'_>, +) -> Result<(), Box<dyn std::error::Error>> { + info!( + "Activating profile `{}` for node `{}`", + profile_name, node_name + ); + + let mut self_activate_command = format!( + "{} activate '{}' '{}'", + deploy_data.current_exe.as_path().to_str().unwrap(), + deploy_data.profile_path, + profile.profile_settings.path, + ); + + if let Some(sudo_cmd) = &deploy_data.sudo { + self_activate_command = format!("{} {}", sudo_cmd, self_activate_command); + } + + if let Some(ref bootstrap_cmd) = profile.profile_settings.bootstrap { + self_activate_command = format!( + "{} --bootstrap-cmd '{}'", + self_activate_command, bootstrap_cmd + ); + } + + if let Some(ref activate_cmd) = profile.profile_settings.activate { + self_activate_command = format!( + "{} --activate-cmd '{}'", + self_activate_command, activate_cmd + ); + } + + let mut c = Command::new("ssh"); + let mut ssh_command = c.arg(format!( + "ssh://{}@{}", + deploy_data.ssh_user, node.node_settings.hostname + )); + + for ssh_opt in &merged_settings.ssh_opts { + ssh_command = ssh_command.arg(ssh_opt); + } + + ssh_command.arg(self_activate_command).spawn()?.await?; + + Ok(()) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..764e2e9 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,71 @@ +use std::borrow::Cow; +use std::path::PathBuf; + +pub mod data; +pub mod deploy; +pub mod push; + +macro_rules! good_panic { + ($($tts:tt)*) => {{ + error!($($tts)*); + std::process::exit(1); + }} +} + +pub struct DeployData<'a> { + pub sudo: Option<String>, + pub ssh_user: Cow<'a, str>, + pub profile_user: Cow<'a, str>, + pub profile_path: String, + pub current_exe: PathBuf, +} + +pub async fn make_deploy_data<'a>( + profile_name: &str, + node_name: &str, + merged_settings: &'a data::GenericSettings, +) -> Result<DeployData<'a>, Box<dyn std::error::Error>> { + let ssh_user: Cow<str> = match &merged_settings.ssh_user { + Some(u) => u.into(), + None => whoami::username().into(), + }; + + let profile_user: Cow<str> = match &merged_settings.user { + Some(x) => x.into(), + None => match &merged_settings.ssh_user { + Some(x) => x.into(), + None => good_panic!( + "Neither user nor sshUser set for profile `{}` of node `{}`", + profile_name, + node_name + ), + }, + }; + + let profile_path = match &profile_user[..] { + "root" => format!("/nix/var/nix/profiles/{}", profile_name), + _ => format!( + "/nix/var/nix/profiles/per-user/{}/{}", + profile_user, profile_name + ), + }; + + let sudo: Option<String> = match merged_settings.user { + Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), + _ => None, + }; + + let current_exe = std::env::current_exe().expect("Expected to find current executable path"); + + if !current_exe.starts_with("/nix/store/") { + good_panic!("The deploy binary must be in the Nix store"); + } + + Ok(DeployData { + sudo, + ssh_user, + profile_user, + profile_path, + current_exe, + }) +} diff --git a/src/utils/push.rs b/src/utils/push.rs new file mode 100644 index 0000000..54ae013 --- /dev/null +++ b/src/utils/push.rs @@ -0,0 +1,108 @@ +use super::data; + +use std::process::Stdio; +use tokio::process::Command; + +pub async fn push_profile( + profile: &data::Profile, + profile_name: &str, + node: &data::Node, + node_name: &str, + supports_flakes: bool, + check_sigs: bool, + repo: &str, + merged_settings: &data::GenericSettings, + deploy_data: &super::DeployData<'_>, +) -> Result<(), Box<dyn std::error::Error>> { + info!( + "Pushing profile `{}` for node `{}`", + profile_name, node_name + ); + + debug!( + "Building profile `{} for node `{}`", + profile_name, node_name + ); + + if supports_flakes { + Command::new("nix") + .arg("build") + .arg("--no-link") + .arg(format!( + "{}#deploy.nodes.{}.profiles.{}.path", + repo, node_name, profile_name + )) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()? + .await?; + } else { + Command::new("nix-build") + .arg(&repo) + .arg("-A") + .arg(format!( + "deploy.nodes.{}.profiles.{}.path", + node_name, profile_name + )) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()? + .await?; + } + + if let Ok(local_key) = std::env::var("LOCAL_KEY") { + info!( + "Signing key present! Signing profile `{}` for node `{}`", + profile_name, node_name + ); + + Command::new("nix") + .arg("sign-paths") + .arg("-r") + .arg("-k") + .arg(local_key) + .arg(&profile.profile_settings.path) + .arg(&deploy_data.current_exe) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()? + .await?; + } + + debug!("Copying profile `{} for node `{}`", profile_name, node_name); + + let mut copy_command_ = Command::new("nix"); + let mut copy_command = copy_command_.arg("copy"); + + if let Some(true) = merged_settings.fast_connection { + copy_command = copy_command.arg("--substitute-on-destination"); + } + + if !check_sigs { + copy_command = copy_command.arg("--no-check-sigs"); + } + + let ssh_opts_str = merged_settings + .ssh_opts + // This should provide some extra safety, but it also breaks for some reason, oh well + // .iter() + // .map(|x| format!("'{}'", x)) + // .collect::<Vec<String>>() + .join(" "); + + copy_command + .arg("--to") + .arg(format!( + "ssh://{}@{}", + deploy_data.ssh_user, node.node_settings.hostname + )) + .arg(&profile.profile_settings.path) + .arg(&deploy_data.current_exe) + .env("NIX_SSHOPTS", ssh_opts_str) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()? + .await?; + + Ok(()) +} |