// SPDX-FileCopyrightText: 2020 Serokell // // SPDX-License-Identifier: MPL-2.0 use std::borrow::Cow; use std::path::PathBuf; use merge::Merge; #[macro_export] macro_rules! good_panic { ($($tts:tt)*) => {{ error!($($tts)*); std::process::exit(1); }} } pub mod data; pub mod deploy; pub mod push; pub struct CmdOverrides { pub ssh_user: Option, pub profile_user: Option, pub ssh_opts: Option, pub fast_connection: Option, pub auto_rollback: Option, pub hostname: Option, } pub enum OverridePurity { ErrorProfile, Error, Warn, Pure, } impl CmdOverrides { pub fn purity(&self) -> OverridePurity { if self.profile_user.is_some() { return OverridePurity::ErrorProfile; } if self.hostname.is_some() || self.ssh_user.is_some() { return OverridePurity::Error; } if self.ssh_opts.is_some() || self.fast_connection.is_some() { return OverridePurity::Warn; } OverridePurity::Pure } } #[derive(PartialEq, Debug)] pub struct DeployFlake<'a> { pub repo: &'a str, pub node: Option<&'a str>, pub profile: Option<&'a str>, } pub fn parse_flake(flake: &str) -> DeployFlake { let flake_fragment_start = flake.find('#'); let (repo, maybe_fragment) = match flake_fragment_start { Some(s) => (&flake[..s], Some(&flake[s + 1..])), None => (flake, None), }; let (node, profile) = match maybe_fragment { Some(fragment) => { let fragment_profile_start = fragment.find('.'); match fragment_profile_start { Some(s) => (Some(&fragment[..s]), Some(&fragment[s + 1..])), None => (Some(fragment), None), } } None => (None, None), }; DeployFlake { repo, node, profile, } } #[test] fn test_parse_flake() { assert_eq!( parse_flake("../deploy/examples/system#example"), DeployFlake { repo: "../deploy/examples/system", node: Some("example"), profile: None } ); assert_eq!( parse_flake("../deploy/examples/system#example.system"), DeployFlake { repo: "../deploy/examples/system", node: Some("example"), profile: Some("system") } ); assert_eq!( parse_flake("../deploy/examples/system"), DeployFlake { repo: "../deploy/examples/system", node: None, profile: None, } ); } pub struct DeployData<'a> { pub node_name: &'a str, pub node: &'a data::Node, pub profile_name: &'a str, pub profile: &'a data::Profile, pub cmd_overrides: &'a CmdOverrides, pub merged_settings: data::GenericSettings, } pub struct DeployDefs<'a> { pub ssh_user: Cow<'a, str>, pub profile_user: Cow<'a, str>, pub profile_path: String, pub current_exe: PathBuf, pub sudo: Option, } impl<'a> DeployData<'a> { pub fn defs(&'a self) -> DeployDefs<'a> { let ssh_user: Cow = match self.merged_settings.ssh_user { Some(ref u) => u.into(), None => whoami::username().into(), }; let profile_user: Cow = match self.merged_settings.user { Some(ref x) => x.into(), None => match self.merged_settings.ssh_user { Some(ref x) => x.into(), None => good_panic!( "Neither user nor sshUser set for profile `{}` of node `{}`", self.profile_name, self.node_name ), }, }; let profile_path = match &profile_user[..] { "root" => format!("/nix/var/nix/profiles/{}", self.profile_name), _ => format!( "/nix/var/nix/profiles/per-user/{}/{}", profile_user, self.profile_name ), }; let sudo: Option = match self.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"); } DeployDefs { ssh_user, profile_user, profile_path, current_exe, sudo, } } } pub fn make_deploy_data<'a, 's>( top_settings: &'s data::GenericSettings, node: &'a data::Node, node_name: &'a str, profile: &'a data::Profile, profile_name: &'a str, cmd_overrides: &'a CmdOverrides, ) -> Result, Box> { let mut merged_settings = top_settings.clone(); merged_settings.merge(node.generic_settings.clone()); merged_settings.merge(profile.generic_settings.clone()); if cmd_overrides.ssh_user.is_some() { merged_settings.ssh_user = cmd_overrides.ssh_user.clone(); } if cmd_overrides.profile_user.is_some() { merged_settings.user = cmd_overrides.profile_user.clone(); } if let Some(ref ssh_opts) = cmd_overrides.ssh_opts { merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect(); } if let Some(fast_connection) = cmd_overrides.fast_connection { merged_settings.fast_connection = fast_connection; } if let Some(auto_rollback) = cmd_overrides.auto_rollback { merged_settings.auto_rollback = auto_rollback; } Ok(DeployData { profile, profile_name, node, node_name, cmd_overrides, merged_settings, }) } pub fn deploy_path_to_activate_path_str( deploy_path: &std::path::Path, ) -> Result> { Ok(format!( "{}/activate", deploy_path .parent() .ok_or("Deploy path too short")? .to_str() .ok_or("Deploy path is not valid utf8")? .to_owned() )) } #[test] fn test_activate_path_generation() { match deploy_path_to_activate_path_str(&std::path::PathBuf::from( "/blah/blah/deploy-rs/bin/deploy", )) { Err(_) => panic!(""), Ok(x) => assert_eq!(x, "/blah/blah/deploy-rs/bin/activate".to_string()), } }