From f201cd0c60ddd9f5cb8564c52e3ee765722a3da1 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sat, 7 Aug 2021 13:45:56 -0500 Subject: [PATCH 01/10] Refactor data to settings (specificity) --- src/cli.rs | 16 ++++++++-------- src/lib.rs | 14 +++++++------- src/{data.rs => settings.rs} | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) rename src/{data.rs => settings.rs} (99%) diff --git a/src/cli.rs b/src/cli.rs index 61890e43..68ac6c97 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use clap::{ArgMatches, Clap, FromArgMatches}; use crate as deploy; -use self::deploy::{DeployFlake, ParseFlakeError}; +use self::deploy::{DeployFlake, ParseFlakeError, settings}; use futures_util::stream::{StreamExt, TryStreamExt}; use log::{debug, error, info, warn}; use serde::Serialize; @@ -170,7 +170,7 @@ async fn get_deployment_data( supports_flakes: bool, flakes: &[deploy::DeployFlake<'_>], extra_build_args: &[String], -) -> Result, GetDeploymentDataError> { +) -> Result, GetDeploymentDataError> { futures_util::stream::iter(flakes).then(|flake| async move { info!("Evaluating flake in {}", flake.repo); @@ -389,14 +389,14 @@ pub enum RunDeployError { type ToDeploy<'a> = Vec<( &'a deploy::DeployFlake<'a>, - &'a deploy::data::Data, - (&'a str, &'a deploy::data::Node), - (&'a str, &'a deploy::data::Profile), + &'a settings::Root, + (&'a str, &'a settings::Node), + (&'a str, &'a settings::Profile), )>; async fn run_deploy( deploy_flakes: Vec>, - data: Vec, + data: Vec, supports_flakes: bool, check_sigs: bool, interactive: bool, @@ -437,7 +437,7 @@ async fn run_deploy( None => return Err(RunDeployError::NodeNotFound(node_name.clone())), }; - let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new(); + let mut profiles_list: Vec<(&str, &settings::Profile)> = Vec::new(); for profile_name in [ node.node_settings.profiles_order.iter().collect(), @@ -466,7 +466,7 @@ async fn run_deploy( let mut l = Vec::new(); for (node_name, node) in &data.nodes { - let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new(); + let mut profiles_list: Vec<(&str, &settings::Profile)> = Vec::new(); for profile_name in [ node.node_settings.profiles_order.iter().collect(), diff --git a/src/lib.rs b/src/lib.rs index 981ec1ed..630df179 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,7 @@ pub fn init_logger( Ok(()) } -pub mod data; +pub mod settings; pub mod deploy; pub mod push; pub mod cli; @@ -312,13 +312,13 @@ fn test_parse_flake() { #[derive(Debug, Clone)] pub struct DeployData<'a> { pub node_name: &'a str, - pub node: &'a data::Node, + pub node: &'a settings::Node, pub profile_name: &'a str, - pub profile: &'a data::Profile, + pub profile: &'a settings::Profile, pub cmd_overrides: &'a CmdOverrides, - pub merged_settings: data::GenericSettings, + pub merged_settings: settings::GenericSettings, pub debug_logs: bool, pub log_dir: Option<&'a str>, @@ -395,10 +395,10 @@ impl<'a> DeployData<'a> { } pub fn make_deploy_data<'a, 's>( - top_settings: &'s data::GenericSettings, - node: &'a data::Node, + top_settings: &'s settings::GenericSettings, + node: &'a settings::Node, node_name: &'a str, - profile: &'a data::Profile, + profile: &'a settings::Profile, profile_name: &'a str, cmd_overrides: &'a CmdOverrides, debug_logs: bool, diff --git a/src/data.rs b/src/settings.rs similarity index 99% rename from src/data.rs rename to src/settings.rs index 6fe7f75f..9ce50a0f 100644 --- a/src/data.rs +++ b/src/settings.rs @@ -66,7 +66,7 @@ pub struct Node { } #[derive(Deserialize, Debug, Clone)] -pub struct Data { +pub struct Root { #[serde(flatten)] pub generic_settings: GenericSettings, pub nodes: HashMap, From c99adbde8c979953a8c5bebe2f811ff634c89787 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sat, 7 Aug 2021 14:33:52 -0500 Subject: [PATCH 02/10] Refactor data structures into thier own module - preparation for a more view based data access --- src/cli.rs | 44 ++++---- src/data.rs | 300 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/deploy.rs | 16 +-- src/lib.rs | 296 +------------------------------------------------ src/push.rs | 6 +- 5 files changed, 335 insertions(+), 327 deletions(-) create mode 100644 src/data.rs diff --git a/src/cli.rs b/src/cli.rs index 68ac6c97..593ed06b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use clap::{ArgMatches, Clap, FromArgMatches}; use crate as deploy; -use self::deploy::{DeployFlake, ParseFlakeError, settings}; +use self::deploy::{data, settings}; use futures_util::stream::{StreamExt, TryStreamExt}; use log::{debug, error, info, warn}; use serde::Serialize; @@ -168,7 +168,7 @@ pub enum GetDeploymentDataError { /// Evaluates the Nix in the given `repo` and return the processed Data from it async fn get_deployment_data( supports_flakes: bool, - flakes: &[deploy::DeployFlake<'_>], + flakes: &[data::DeployFlake<'_>], extra_build_args: &[String], ) -> Result, GetDeploymentDataError> { futures_util::stream::iter(flakes).then(|flake| async move { @@ -272,9 +272,9 @@ struct PromptPart<'a> { fn print_deployment( parts: &[( - &deploy::DeployFlake<'_>, - deploy::DeployData, - deploy::DeployDefs, + &data::DeployFlake<'_>, + data::DeployData, + data::DeployDefs, )], ) -> Result<(), toml::ser::Error> { let mut part_map: HashMap> = HashMap::new(); @@ -315,9 +315,9 @@ pub enum PromptDeploymentError { fn prompt_deployment( parts: &[( - &deploy::DeployFlake<'_>, - deploy::DeployData, - deploy::DeployDefs, + &data::DeployFlake<'_>, + data::DeployData, + data::DeployDefs, )], ) -> Result<(), PromptDeploymentError> { print_deployment(parts)?; @@ -378,7 +378,7 @@ pub enum RunDeployError { #[error("Profile was provided without a node name")] ProfileWithoutNode, #[error("Error processing deployment definitions: {0}")] - DeployDataDefs(#[from] deploy::DeployDataDefsError), + InvalidDeployDataDefs(#[from] data::DeployDataDefsError), #[error("Failed to make printable TOML of deployment: {0}")] TomlFormat(#[from] toml::ser::Error), #[error("{0}")] @@ -388,19 +388,19 @@ pub enum RunDeployError { } type ToDeploy<'a> = Vec<( - &'a deploy::DeployFlake<'a>, + &'a data::DeployFlake<'a>, &'a settings::Root, (&'a str, &'a settings::Node), (&'a str, &'a settings::Profile), )>; async fn run_deploy( - deploy_flakes: Vec>, + deploy_flakes: Vec>, data: Vec, supports_flakes: bool, check_sigs: bool, interactive: bool, - cmd_overrides: &deploy::CmdOverrides, + cmd_overrides: &data::CmdOverrides, keep_result: bool, result_path: Option<&str>, extra_build_args: &[String], @@ -508,13 +508,13 @@ async fn run_deploy( .collect(); let mut parts: Vec<( - &deploy::DeployFlake<'_>, - deploy::DeployData, - deploy::DeployDefs, + &data::DeployFlake<'_>, + data::DeployData, + data::DeployDefs, )> = Vec::new(); for (deploy_flake, data, (node_name, node), (profile_name, profile)) in to_deploy { - let deploy_data = deploy::make_deploy_data( + let deploy_data = data::make_deploy_data( &data.generic_settings, node, node_name, @@ -550,7 +550,7 @@ async fn run_deploy( .await?; } - let mut succeeded: Vec<(&deploy::DeployData, &deploy::DeployDefs)> = vec![]; + let mut succeeded: Vec<(&data::DeployData, &data::DeployDefs)> = vec![]; // Run all deployments // In case of an error rollback any previoulsy made deployment. @@ -595,7 +595,7 @@ pub enum RunError { #[error("Failed to evaluate deployment data: {0}")] GetDeploymentData(#[from] GetDeploymentDataError), #[error("Error parsing flake: {0}")] - ParseFlake(#[from] deploy::ParseFlakeError), + ParseFlake(#[from] data::ParseFlakeError), #[error("Error initiating logger: {0}")] Logger(#[from] flexi_logger::FlexiLoggerError), #[error("{0}")] @@ -619,12 +619,12 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { .targets .unwrap_or_else(|| vec![opts.clone().target.unwrap_or_else(|| ".".to_string())]); - let deploy_flakes: Vec = deploys + let deploy_flakes: Vec = deploys .iter() - .map(|f| deploy::parse_flake(f.as_str())) - .collect::, ParseFlakeError>>()?; + .map(|f| data::parse_flake(f.as_str())) + .collect::, data::ParseFlakeError>>()?; - let cmd_overrides = deploy::CmdOverrides { + let cmd_overrides = data::CmdOverrides { ssh_user: opts.ssh_user, profile_user: opts.profile_user, ssh_opts: opts.ssh_opts, diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 00000000..86e1b6cd --- /dev/null +++ b/src/data.rs @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: 2020 Serokell +// SPDX-FileCopyrightText: 2021 Yannik Sander +// +// SPDX-License-Identifier: MPL-2.0 + +use rnix::{types::*, SyntaxKind::*}; +use merge::Merge; +use thiserror::Error; + +use crate::settings; + +#[derive(PartialEq, Debug)] +pub struct DeployFlake<'a> { + pub repo: &'a str, + pub node: Option, + pub profile: Option, +} + +#[derive(Error, Debug)] +pub enum ParseFlakeError { + #[error("The given path was too long, did you mean to put something in quotes?")] + PathTooLong, + #[error("Unrecognized node or token encountered")] + Unrecognized, +} + +pub fn parse_flake(flake: &str) -> Result { + 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 mut node: Option = None; + let mut profile: Option = None; + + if let Some(fragment) = maybe_fragment { + let ast = rnix::parse(fragment); + + let first_child = match ast.root().node().first_child() { + Some(x) => x, + None => { + return Ok(DeployFlake { + repo, + node: None, + profile: None, + }) + } + }; + + let mut node_over = false; + + for entry in first_child.children_with_tokens() { + let x: Option = match (entry.kind(), node_over) { + (TOKEN_DOT, false) => { + node_over = true; + None + } + (TOKEN_DOT, true) => { + return Err(ParseFlakeError::PathTooLong); + } + (NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()), + (TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()), + (NODE_STRING, _) => { + let c = entry + .into_node() + .unwrap() + .children_with_tokens() + .nth(1) + .unwrap(); + + Some(c.into_token().unwrap().text().to_string()) + } + _ => return Err(ParseFlakeError::Unrecognized), + }; + + if !node_over { + node = x; + } else { + profile = x; + } + } + } + + Ok(DeployFlake { + repo, + node, + profile, + }) +} + +#[test] +fn test_parse_flake() { + assert_eq!( + parse_flake("../deploy/examples/system").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: None, + profile: None, + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system#").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: None, + profile: None, + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system#computer.\"something.nix\"").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: Some("computer".to_string()), + profile: Some("something.nix".to_string()), + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system#\"example.com\".system").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: Some("example.com".to_string()), + profile: Some("system".to_string()), + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system#example").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: Some("example".to_string()), + profile: None + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system#example.system").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: Some("example".to_string()), + profile: Some("system".to_string()) + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: None, + profile: None, + } + ); +} + +#[derive(Debug)] +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 magic_rollback: Option, + pub temp_path: Option, + pub confirm_timeout: Option, + pub dry_activate: bool, +} + +#[derive(Debug, Clone)] +pub struct DeployData<'a> { + pub node_name: &'a str, + pub node: &'a settings::Node, + pub profile_name: &'a str, + pub profile: &'a settings::Profile, + + pub cmd_overrides: &'a CmdOverrides, + + pub merged_settings: settings::GenericSettings, + + pub debug_logs: bool, + pub log_dir: Option<&'a str>, +} + +#[derive(Debug)] +pub struct DeployDefs { + pub ssh_user: String, + pub profile_user: String, + pub profile_path: String, + pub sudo: Option, +} + +#[derive(Error, Debug)] +pub enum DeployDataDefsError { + #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")] + NoProfileUser(String, String), +} + +impl<'a> DeployData<'a> { + pub fn defs(&'a self) -> Result { + let ssh_user = match self.merged_settings.ssh_user { + Some(ref u) => u.clone(), + None => whoami::username(), + }; + + let profile_user = self.get_profile_user()?; + + let profile_path = self.get_profile_path()?; + + let sudo: Option = match self.merged_settings.user { + Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), + _ => None, + }; + + Ok(DeployDefs { + ssh_user, + profile_user, + profile_path, + sudo, + }) + } + + pub fn get_profile_path(&'a self) -> Result { + let profile_user = self.get_profile_user()?; + let profile_path = match self.profile.profile_settings.profile_path { + None => match &profile_user[..] { + "root" => format!("/nix/var/nix/profiles/{}", self.profile_name), + _ => format!( + "/nix/var/nix/profiles/per-user/{}/{}", + profile_user, self.profile_name + ), + }, + Some(ref x) => x.clone(), + }; + Ok(profile_path) + } + + pub fn get_profile_user(&'a self) -> Result { + let profile_user = match self.merged_settings.user { + Some(ref x) => x.clone(), + None => match self.merged_settings.ssh_user { + Some(ref x) => x.clone(), + None => { + return Err(DeployDataDefsError::NoProfileUser( + self.profile_name.to_owned(), + self.node_name.to_owned(), + )) + } + }, + }; + Ok(profile_user) + } +} + +pub fn make_deploy_data<'a, 's>( + top_settings: &'s settings::GenericSettings, + node: &'a settings::Node, + node_name: &'a str, + profile: &'a settings::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()); + merged_settings.merge(top_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 = Some(fast_connection); + } + if let Some(auto_rollback) = cmd_overrides.auto_rollback { + merged_settings.auto_rollback = Some(auto_rollback); + } + if let Some(magic_rollback) = cmd_overrides.magic_rollback { + merged_settings.magic_rollback = Some(magic_rollback); + } + + DeployData { + node_name, + node, + profile_name, + profile, + cmd_overrides, + merged_settings, + debug_logs, + log_dir, + } +} diff --git a/src/deploy.rs b/src/deploy.rs index f8fc2f90..7c1048ea 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -9,7 +9,7 @@ use std::borrow::Cow; use thiserror::Error; use tokio::process::Command; -use crate::DeployDataDefsError; +use crate::data; struct ActivateCommandData<'a> { sudo: &'a Option, @@ -207,8 +207,8 @@ pub enum ConfirmProfileError { } pub async fn confirm_profile( - deploy_data: &super::DeployData<'_>, - deploy_defs: &super::DeployDefs, + deploy_data: &data::DeployData<'_>, + deploy_defs: &data::DeployDefs, temp_path: Cow<'_, str>, ssh_addr: &str, ) -> Result<(), ConfirmProfileError> { @@ -267,8 +267,8 @@ pub enum DeployProfileError { } pub async fn deploy_profile( - deploy_data: &super::DeployData<'_>, - deploy_defs: &super::DeployDefs, + deploy_data: &data::DeployData<'_>, + deploy_defs: &data::DeployDefs, dry_activate: bool, ) -> Result<(), DeployProfileError> { if !dry_activate { @@ -415,11 +415,11 @@ pub enum RevokeProfileError { SSHRevokeExit(Option), #[error("Deployment data invalid: {0}")] - InvalidDeployDataDefs(#[from] DeployDataDefsError), + InvalidDeployDataDefs(#[from] data::DeployDataDefsError), } pub async fn revoke( - deploy_data: &crate::DeployData<'_>, - deploy_defs: &crate::DeployDefs, + deploy_data: &data::DeployData<'_>, + deploy_defs: &data::DeployDefs, ) -> Result<(), RevokeProfileError> { let self_revoke_command = build_revoke_command(&RevokeCommandData { sudo: &deploy_defs.sudo, diff --git a/src/lib.rs b/src/lib.rs index 630df179..5cd69f81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,6 @@ // // SPDX-License-Identifier: MPL-2.0 -use rnix::{types::*, SyntaxKind::*}; - -use merge::Merge; - -use thiserror::Error; - use flexi_logger::*; pub fn make_lock_path(temp_path: &str, closure: &str) -> String { @@ -146,295 +140,7 @@ pub fn init_logger( } pub mod settings; +pub mod data; pub mod deploy; pub mod push; pub mod cli; - -#[derive(Debug)] -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 magic_rollback: Option, - pub temp_path: Option, - pub confirm_timeout: Option, - pub dry_activate: bool, -} - -#[derive(PartialEq, Debug)] -pub struct DeployFlake<'a> { - pub repo: &'a str, - pub node: Option, - pub profile: Option, -} - -#[derive(Error, Debug)] -pub enum ParseFlakeError { - #[error("The given path was too long, did you mean to put something in quotes?")] - PathTooLong, - #[error("Unrecognized node or token encountered")] - Unrecognized, -} -pub fn parse_flake(flake: &str) -> Result { - 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 mut node: Option = None; - let mut profile: Option = None; - - if let Some(fragment) = maybe_fragment { - let ast = rnix::parse(fragment); - - let first_child = match ast.root().node().first_child() { - Some(x) => x, - None => { - return Ok(DeployFlake { - repo, - node: None, - profile: None, - }) - } - }; - - let mut node_over = false; - - for entry in first_child.children_with_tokens() { - let x: Option = match (entry.kind(), node_over) { - (TOKEN_DOT, false) => { - node_over = true; - None - } - (TOKEN_DOT, true) => { - return Err(ParseFlakeError::PathTooLong); - } - (NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()), - (TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()), - (NODE_STRING, _) => { - let c = entry - .into_node() - .unwrap() - .children_with_tokens() - .nth(1) - .unwrap(); - - Some(c.into_token().unwrap().text().to_string()) - } - _ => return Err(ParseFlakeError::Unrecognized), - }; - - if !node_over { - node = x; - } else { - profile = x; - } - } - } - - Ok(DeployFlake { - repo, - node, - profile, - }) -} - -#[test] -fn test_parse_flake() { - assert_eq!( - parse_flake("../deploy/examples/system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: None, - profile: None, - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: None, - profile: None, - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#computer.\"something.nix\"").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("computer".to_string()), - profile: Some("something.nix".to_string()), - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#\"example.com\".system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("example.com".to_string()), - profile: Some("system".to_string()), - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#example").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("example".to_string()), - profile: None - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#example.system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("example".to_string()), - profile: Some("system".to_string()) - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: None, - profile: None, - } - ); -} - -#[derive(Debug, Clone)] -pub struct DeployData<'a> { - pub node_name: &'a str, - pub node: &'a settings::Node, - pub profile_name: &'a str, - pub profile: &'a settings::Profile, - - pub cmd_overrides: &'a CmdOverrides, - - pub merged_settings: settings::GenericSettings, - - pub debug_logs: bool, - pub log_dir: Option<&'a str>, -} - -#[derive(Debug)] -pub struct DeployDefs { - pub ssh_user: String, - pub profile_user: String, - pub profile_path: String, - pub sudo: Option, -} - -#[derive(Error, Debug)] -pub enum DeployDataDefsError { - #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")] - NoProfileUser(String, String), -} - -impl<'a> DeployData<'a> { - pub fn defs(&'a self) -> Result { - let ssh_user = match self.merged_settings.ssh_user { - Some(ref u) => u.clone(), - None => whoami::username(), - }; - - let profile_user = self.get_profile_user()?; - - let profile_path = self.get_profile_path()?; - - let sudo: Option = match self.merged_settings.user { - Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), - _ => None, - }; - - Ok(DeployDefs { - ssh_user, - profile_user, - profile_path, - sudo, - }) - } - - fn get_profile_path(&'a self) -> Result { - let profile_user = self.get_profile_user()?; - let profile_path = match self.profile.profile_settings.profile_path { - None => match &profile_user[..] { - "root" => format!("/nix/var/nix/profiles/{}", self.profile_name), - _ => format!( - "/nix/var/nix/profiles/per-user/{}/{}", - profile_user, self.profile_name - ), - }, - Some(ref x) => x.clone(), - }; - Ok(profile_path) - } - - fn get_profile_user(&'a self) -> Result { - let profile_user = match self.merged_settings.user { - Some(ref x) => x.clone(), - None => match self.merged_settings.ssh_user { - Some(ref x) => x.clone(), - None => { - return Err(DeployDataDefsError::NoProfileUser( - self.profile_name.to_owned(), - self.node_name.to_owned(), - )) - } - }, - }; - Ok(profile_user) - } -} - -pub fn make_deploy_data<'a, 's>( - top_settings: &'s settings::GenericSettings, - node: &'a settings::Node, - node_name: &'a str, - profile: &'a settings::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()); - merged_settings.merge(top_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 = Some(fast_connection); - } - if let Some(auto_rollback) = cmd_overrides.auto_rollback { - merged_settings.auto_rollback = Some(auto_rollback); - } - if let Some(magic_rollback) = cmd_overrides.magic_rollback { - merged_settings.magic_rollback = Some(magic_rollback); - } - - DeployData { - node_name, - node, - profile_name, - profile, - cmd_overrides, - merged_settings, - debug_logs, - log_dir, - } -} diff --git a/src/push.rs b/src/push.rs index 69eba0db..ee55a123 100644 --- a/src/push.rs +++ b/src/push.rs @@ -9,6 +9,8 @@ use std::process::Stdio; use thiserror::Error; use tokio::process::Command; +use crate::data; + #[derive(Error, Debug)] pub enum PushProfileError { #[error("Failed to run Nix show-derivation command: {0}")] @@ -47,8 +49,8 @@ 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 deploy_data: &'a data::DeployData<'a>, + pub deploy_defs: &'a data::DeployDefs, pub keep_result: bool, pub result_path: Option<&'a str>, pub extra_build_args: &'a [String], From a10cc65a45b5781af2f2df1e47e8013aa4d47847 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sat, 7 Aug 2021 15:38:25 -0500 Subject: [PATCH 03/10] Refactor implement from_str trait for `Target` data --- src/cli.rs | 48 ++++++++-------- src/data.rs | 163 ++++++++++++++++++++++++++-------------------------- 2 files changed, 107 insertions(+), 104 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 593ed06b..002f2f84 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -168,7 +168,7 @@ pub enum GetDeploymentDataError { /// Evaluates the Nix in the given `repo` and return the processed Data from it async fn get_deployment_data( supports_flakes: bool, - flakes: &[data::DeployFlake<'_>], + flakes: &[data::Target], extra_build_args: &[String], ) -> Result, GetDeploymentDataError> { futures_util::stream::iter(flakes).then(|flake| async move { @@ -272,7 +272,7 @@ struct PromptPart<'a> { fn print_deployment( parts: &[( - &data::DeployFlake<'_>, + &data::Target, data::DeployData, data::DeployDefs, )], @@ -315,7 +315,7 @@ pub enum PromptDeploymentError { fn prompt_deployment( parts: &[( - &data::DeployFlake<'_>, + &data::Target, data::DeployData, data::DeployDefs, )], @@ -388,14 +388,14 @@ pub enum RunDeployError { } type ToDeploy<'a> = Vec<( - &'a data::DeployFlake<'a>, + &'a data::Target, &'a settings::Root, (&'a str, &'a settings::Node), (&'a str, &'a settings::Profile), )>; async fn run_deploy( - deploy_flakes: Vec>, + deploy_targets: Vec, data: Vec, supports_flakes: bool, check_sigs: bool, @@ -409,11 +409,11 @@ async fn run_deploy( log_dir: &Option, rollback_succeeded: bool, ) -> Result<(), RunDeployError> { - let to_deploy: ToDeploy = deploy_flakes + let to_deploy: ToDeploy = deploy_targets .iter() .zip(&data) - .map(|(deploy_flake, data)| { - let to_deploys: ToDeploy = match (&deploy_flake.node, &deploy_flake.profile) { + .map(|(deploy_target, data)| { + let to_deploys: ToDeploy = match (&deploy_target.node, &deploy_target.profile) { (Some(node_name), Some(profile_name)) => { let node = match data.nodes.get(node_name) { Some(x) => x, @@ -425,7 +425,7 @@ async fn run_deploy( }; vec![( - deploy_flake, + deploy_target, data, (node_name.as_str(), node), (profile_name.as_str(), profile), @@ -459,7 +459,7 @@ async fn run_deploy( profiles_list .into_iter() - .map(|x| (deploy_flake, data, (node_name.as_str(), node), x)) + .map(|x| (deploy_target, data, (node_name.as_str(), node), x)) .collect() } (None, None) => { @@ -490,7 +490,7 @@ async fn run_deploy( let ll: ToDeploy = profiles_list .into_iter() - .map(|x| (deploy_flake, data, (node_name.as_str(), node), x)) + .map(|x| (deploy_target, data, (node_name.as_str(), node), x)) .collect(); l.extend(ll); @@ -508,12 +508,12 @@ async fn run_deploy( .collect(); let mut parts: Vec<( - &data::DeployFlake<'_>, + &data::Target, data::DeployData, data::DeployDefs, )> = Vec::new(); - for (deploy_flake, data, (node_name, node), (profile_name, profile)) in to_deploy { + for (deploy_target, data, (node_name, node), (profile_name, profile)) in to_deploy { let deploy_data = data::make_deploy_data( &data.generic_settings, node, @@ -527,7 +527,7 @@ async fn run_deploy( let deploy_defs = deploy_data.defs()?; - parts.push((deploy_flake, deploy_data, deploy_defs)); + parts.push((deploy_target, deploy_data, deploy_defs)); } if interactive { @@ -536,11 +536,11 @@ async fn run_deploy( print_deployment(&parts[..])?; } - for (deploy_flake, deploy_data, deploy_defs) in &parts { + for (deploy_target, deploy_data, deploy_defs) in &parts { deploy::push::push_profile(deploy::push::PushProfileData { supports_flakes, check_sigs, - repo: deploy_flake.repo, + repo: &deploy_target.repo, deploy_data, deploy_defs, keep_result, @@ -595,7 +595,7 @@ pub enum RunError { #[error("Failed to evaluate deployment data: {0}")] GetDeploymentData(#[from] GetDeploymentDataError), #[error("Error parsing flake: {0}")] - ParseFlake(#[from] data::ParseFlakeError), + ParseFlake(#[from] data::ParseTargetError), #[error("Error initiating logger: {0}")] Logger(#[from] flexi_logger::FlexiLoggerError), #[error("{0}")] @@ -619,10 +619,10 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { .targets .unwrap_or_else(|| vec![opts.clone().target.unwrap_or_else(|| ".".to_string())]); - let deploy_flakes: Vec = deploys + let deploy_targets: Vec = deploys .iter() - .map(|f| data::parse_flake(f.as_str())) - .collect::, data::ParseFlakeError>>()?; + .map(|f| f.parse::()) + .collect::, data::ParseTargetError>>()?; let cmd_overrides = data::CmdOverrides { ssh_user: opts.ssh_user, @@ -644,14 +644,14 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { } if !opts.skip_checks { - for deploy_flake in &deploy_flakes { - check_deployment(supports_flakes, deploy_flake.repo, &opts.extra_build_args).await?; + for deploy_target in deploy_targets.iter() { + check_deployment(supports_flakes, &deploy_target.repo, &opts.extra_build_args).await?; } } let result_path = opts.result_path.as_deref(); - let data = get_deployment_data(supports_flakes, &deploy_flakes, &opts.extra_build_args).await?; + let data = get_deployment_data(supports_flakes, &deploy_targets, &opts.extra_build_args).await?; run_deploy( - deploy_flakes, + deploy_targets, data, supports_flakes, opts.checksigs, diff --git a/src/data.rs b/src/data.rs index 86e1b6cd..9de663ef 100644 --- a/src/data.rs +++ b/src/data.rs @@ -10,145 +10,148 @@ use thiserror::Error; use crate::settings; #[derive(PartialEq, Debug)] -pub struct DeployFlake<'a> { - pub repo: &'a str, +pub struct Target { + pub repo: String, pub node: Option, pub profile: Option, } #[derive(Error, Debug)] -pub enum ParseFlakeError { +pub enum ParseTargetError { #[error("The given path was too long, did you mean to put something in quotes?")] PathTooLong, #[error("Unrecognized node or token encountered")] Unrecognized, } - -pub fn parse_flake(flake: &str) -> Result { - 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 mut node: Option = None; - let mut profile: Option = None; - - if let Some(fragment) = maybe_fragment { - let ast = rnix::parse(fragment); - - let first_child = match ast.root().node().first_child() { - Some(x) => x, - None => { - return Ok(DeployFlake { - repo, - node: None, - profile: None, - }) - } +impl std::str::FromStr for Target { + type Err = ParseTargetError; + + fn from_str(s: &str) -> Result { + let flake_fragment_start = s.find('#'); + let (repo, maybe_fragment) = match flake_fragment_start { + Some(i) => (s[..i].to_string(), Some(&s[i + 1..])), + None => (s.to_string(), None), }; - let mut node_over = false; + let mut node: Option = None; + let mut profile: Option = None; - for entry in first_child.children_with_tokens() { - let x: Option = match (entry.kind(), node_over) { - (TOKEN_DOT, false) => { - node_over = true; - None - } - (TOKEN_DOT, true) => { - return Err(ParseFlakeError::PathTooLong); - } - (NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()), - (TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()), - (NODE_STRING, _) => { - let c = entry - .into_node() - .unwrap() - .children_with_tokens() - .nth(1) - .unwrap(); - - Some(c.into_token().unwrap().text().to_string()) + if let Some(fragment) = maybe_fragment { + let ast = rnix::parse(fragment); + + let first_child = match ast.root().node().first_child() { + Some(x) => x, + None => { + return Ok(Target { + repo, + node: None, + profile: None, + }) } - _ => return Err(ParseFlakeError::Unrecognized), }; - if !node_over { - node = x; - } else { - profile = x; + let mut node_over = false; + + for entry in first_child.children_with_tokens() { + let x: Option = match (entry.kind(), node_over) { + (TOKEN_DOT, false) => { + node_over = true; + None + } + (TOKEN_DOT, true) => { + return Err(ParseTargetError::PathTooLong); + } + (NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()), + (TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()), + (NODE_STRING, _) => { + let c = entry + .into_node() + .unwrap() + .children_with_tokens() + .nth(1) + .unwrap(); + + Some(c.into_token().unwrap().text().to_string()) + } + _ => return Err(ParseTargetError::Unrecognized), + }; + + if !node_over { + node = x; + } else { + profile = x; + } } } - } - Ok(DeployFlake { - repo, - node, - profile, - }) + Ok(Target { + repo, + node, + profile, + }) + } } #[test] -fn test_parse_flake() { +fn test_deploy_target_from_str() { assert_eq!( - parse_flake("../deploy/examples/system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", + "../deploy/examples/system".parse::().unwrap(), + Target { + repo: "../deploy/examples/system".to_string(), node: None, profile: None, } ); assert_eq!( - parse_flake("../deploy/examples/system#").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", + "../deploy/examples/system#".parse::().unwrap(), + Target { + repo: "../deploy/examples/system".to_string(), node: None, profile: None, } ); assert_eq!( - parse_flake("../deploy/examples/system#computer.\"something.nix\"").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", + "../deploy/examples/system#computer.\"something.nix\"".parse::().unwrap(), + Target { + repo: "../deploy/examples/system".to_string(), node: Some("computer".to_string()), profile: Some("something.nix".to_string()), } ); assert_eq!( - parse_flake("../deploy/examples/system#\"example.com\".system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", + "../deploy/examples/system#\"example.com\".system".parse::().unwrap(), + Target { + repo: "../deploy/examples/system".to_string(), node: Some("example.com".to_string()), profile: Some("system".to_string()), } ); assert_eq!( - parse_flake("../deploy/examples/system#example").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", + "../deploy/examples/system#example".parse::().unwrap(), + Target { + repo: "../deploy/examples/system".to_string(), node: Some("example".to_string()), profile: None } ); assert_eq!( - parse_flake("../deploy/examples/system#example.system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", + "../deploy/examples/system#example.system".parse::().unwrap(), + Target { + repo: "../deploy/examples/system".to_string(), node: Some("example".to_string()), profile: Some("system".to_string()) } ); assert_eq!( - parse_flake("../deploy/examples/system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", + "../deploy/examples/system".parse::().unwrap(), + Target { + repo: "../deploy/examples/system".to_string(), node: None, profile: None, } From aff6f1fedd13623643175dbbc50810def4779681 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 8 Aug 2021 13:09:18 -0500 Subject: [PATCH 04/10] Refactor move flake parsing into adapter file --- src/cli.rs | 165 ++------------------------------------------------ src/flake.rs | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 173 insertions(+), 160 deletions(-) create mode 100644 src/flake.rs diff --git a/src/cli.rs b/src/cli.rs index 002f2f84..c7d68a46 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,8 +10,7 @@ use clap::{ArgMatches, Clap, FromArgMatches}; use crate as deploy; -use self::deploy::{data, settings}; -use futures_util::stream::{StreamExt, TryStreamExt}; +use self::deploy::{data, settings, flake}; use log::{debug, error, info, warn}; use serde::Serialize; use std::process::Stdio; @@ -107,160 +106,6 @@ async fn test_flake_support() -> Result { .success()) } -#[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:?}")] - NixCheckExit(Option), -} - -async fn check_deployment( - supports_flakes: bool, - repo: &str, - extra_build_args: &[String], -) -> Result<(), CheckDeploymentError> { - info!("Running checks for flake in {}", repo); - - let mut check_command = match supports_flakes { - true => Command::new("nix"), - false => Command::new("nix-build"), - }; - - if supports_flakes { - check_command.arg("flake").arg("check").arg(repo); - } else { - check_command.arg("-E") - .arg("--no-out-link") - .arg(format!("let r = import {}/.; x = (if builtins.isFunction r then (r {{}}) else r); in if x ? checks then x.checks.${{builtins.currentSystem}} else {{}}", repo)); - } - - for extra_arg in extra_build_args { - check_command.arg(extra_arg); - } - - let check_status = check_command.status().await?; - - match check_status.code() { - Some(0) => (), - a => return Err(CheckDeploymentError::NixCheckExit(a)), - }; - - Ok(()) -} - -#[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:?}")] - NixEvalExit(Option), - #[error("Error converting evaluation output to utf8: {0}")] - DecodeUtf8(#[from] std::string::FromUtf8Error), - #[error("Error decoding the JSON from evaluation: {0}")] - DecodeJson(#[from] serde_json::error::Error), - #[error("Impossible happened: profile is set but node is not")] - ProfileNoNode, -} - -/// Evaluates the Nix in the given `repo` and return the processed Data from it -async fn get_deployment_data( - supports_flakes: bool, - flakes: &[data::Target], - extra_build_args: &[String], -) -> Result, GetDeploymentDataError> { - futures_util::stream::iter(flakes).then(|flake| async move { - - info!("Evaluating flake in {}", flake.repo); - - let mut c = if supports_flakes { - Command::new("nix") - } else { - Command::new("nix-instantiate") - }; - - if supports_flakes { - c.arg("eval") - .arg("--json") - .arg(format!("{}#deploy", flake.repo)) - // We use --apply instead of --expr so that we don't have to deal with builtins.getFlake - .arg("--apply"); - match (&flake.node, &flake.profile) { - (Some(node), Some(profile)) => { - // Ignore all nodes and all profiles but the one we're evaluating - c.arg(format!( - r#" - deploy: - (deploy // {{ - nodes = {{ - "{0}" = deploy.nodes."{0}" // {{ - profiles = {{ - inherit (deploy.nodes."{0}".profiles) "{1}"; - }}; - }}; - }}; - }}) - "#, - node, profile - )) - } - (Some(node), None) => { - // Ignore all nodes but the one we're evaluating - c.arg(format!( - r#" - deploy: - (deploy // {{ - nodes = {{ - inherit (deploy.nodes) "{}"; - }}; - }}) - "#, - node - )) - } - (None, None) => { - // We need to evaluate all profiles of all nodes anyway, so just do it strictly - c.arg("deploy: deploy") - } - (None, Some(_)) => return Err(GetDeploymentDataError::ProfileNoNode), - } - } else { - c - .arg("--strict") - .arg("--read-write-mode") - .arg("--json") - .arg("--eval") - .arg("-E") - .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo)) - }; - - for extra_arg in extra_build_args { - c.arg(extra_arg); - } - - let build_child = c - .stdout(Stdio::piped()) - .spawn() - .map_err(GetDeploymentDataError::NixEval)?; - - let build_output = build_child - .wait_with_output() - .await - .map_err(GetDeploymentDataError::NixEvalOut)?; - - match build_output.status.code() { - Some(0) => (), - a => return Err(GetDeploymentDataError::NixEvalExit(a)), - }; - - let data_json = String::from_utf8(build_output.stdout)?; - - Ok(serde_json::from_str(&data_json)?) -}).try_collect().await -} - #[derive(Serialize)] struct PromptPart<'a> { user: &'a str, @@ -591,9 +436,9 @@ pub enum RunError { #[error("Failed to test for flake support: {0}")] FlakeTest(std::io::Error), #[error("Failed to check deployment: {0}")] - CheckDeployment(#[from] CheckDeploymentError), + CheckDeployment(#[from] flake::CheckDeploymentError), #[error("Failed to evaluate deployment data: {0}")] - GetDeploymentData(#[from] GetDeploymentDataError), + GetDeploymentData(#[from] flake::GetDeploymentDataError), #[error("Error parsing flake: {0}")] ParseFlake(#[from] data::ParseTargetError), #[error("Error initiating logger: {0}")] @@ -645,11 +490,11 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { if !opts.skip_checks { for deploy_target in deploy_targets.iter() { - check_deployment(supports_flakes, &deploy_target.repo, &opts.extra_build_args).await?; + flake::check_deployment(supports_flakes, &deploy_target.repo, &opts.extra_build_args).await?; } } let result_path = opts.result_path.as_deref(); - let data = get_deployment_data(supports_flakes, &deploy_targets, &opts.extra_build_args).await?; + let data = flake::get_deployment_data(supports_flakes, &deploy_targets, &opts.extra_build_args).await?; run_deploy( deploy_targets, data, diff --git a/src/flake.rs b/src/flake.rs new file mode 100644 index 00000000..22b6de23 --- /dev/null +++ b/src/flake.rs @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: 2020 Serokell +// SPDX-FileCopyrightText: 2021 Yannik Sander +// +// SPDX-License-Identifier: MPL-2.0 + +use crate as deploy; + +use self::deploy::{data, settings}; +use log::{error, info}; +use std::process::Stdio; +use futures_util::stream::{StreamExt, TryStreamExt}; +use thiserror::Error; +use tokio::process::Command; + +#[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:?}")] + NixCheckExit(Option), +} + +pub async fn check_deployment( + supports_flakes: bool, + repo: &str, + extra_build_args: &[String], +) -> Result<(), CheckDeploymentError> { + info!("Running checks for flake in {}", repo); + + let mut check_command = match supports_flakes { + true => Command::new("nix"), + false => Command::new("nix-build"), + }; + + if supports_flakes { + check_command.arg("flake").arg("check").arg(repo); + } else { + check_command.arg("-E") + .arg("--no-out-link") + .arg(format!("let r = import {}/.; x = (if builtins.isFunction r then (r {{}}) else r); in if x ? checks then x.checks.${{builtins.currentSystem}} else {{}}", repo)); + }; + + for extra_arg in extra_build_args { + check_command.arg(extra_arg); + } + + let check_status = check_command.status().await?; + + match check_status.code() { + Some(0) => (), + a => return Err(CheckDeploymentError::NixCheckExit(a)), + }; + + Ok(()) +} + +#[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:?}")] + NixEvalExit(Option), + #[error("Error converting evaluation output to utf8: {0}")] + DecodeUtf8(#[from] std::string::FromUtf8Error), + #[error("Error decoding the JSON from evaluation: {0}")] + DecodeJson(#[from] serde_json::error::Error), + #[error("Impossible happened: profile is set but node is not")] + ProfileNoNode, +} + +/// Evaluates the Nix in the given `repo` and return the processed Data from it +pub async fn get_deployment_data( + supports_flakes: bool, + flakes: &[data::Target], + extra_build_args: &[String], +) -> Result, GetDeploymentDataError> { + futures_util::stream::iter(flakes).then(|flake| async move { + + info!("Evaluating flake in {}", flake.repo); + + let mut c = if supports_flakes { + Command::new("nix") + } else { + Command::new("nix-instantiate") + }; + + if supports_flakes { + c.arg("eval") + .arg("--json") + .arg(format!("{}#deploy", flake.repo)) + // We use --apply instead of --expr so that we don't have to deal with builtins.getFlake + .arg("--apply"); + match (&flake.node, &flake.profile) { + (Some(node), Some(profile)) => { + // Ignore all nodes and all profiles but the one we're evaluating + c.arg(format!( + r#" + deploy: + (deploy // {{ + nodes = {{ + "{0}" = deploy.nodes."{0}" // {{ + profiles = {{ + inherit (deploy.nodes."{0}".profiles) "{1}"; + }}; + }}; + }}; + }}) + "#, + node, profile + )) + } + (Some(node), None) => { + // Ignore all nodes but the one we're evaluating + c.arg(format!( + r#" + deploy: + (deploy // {{ + nodes = {{ + inherit (deploy.nodes) "{}"; + }}; + }}) + "#, + node + )) + } + (None, None) => { + // We need to evaluate all profiles of all nodes anyway, so just do it strictly + c.arg("deploy: deploy") + } + (None, Some(_)) => return Err(GetDeploymentDataError::ProfileNoNode), + } + } else { + c + .arg("--strict") + .arg("--read-write-mode") + .arg("--json") + .arg("--eval") + .arg("-E") + .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo)) + }; + + for extra_arg in extra_build_args { + c.arg(extra_arg); + } + + let build_child = c + .stdout(Stdio::piped()) + .spawn() + .map_err(GetDeploymentDataError::NixEval)?; + + let build_output = build_child + .wait_with_output() + .await + .map_err(GetDeploymentDataError::NixEvalOut)?; + + match build_output.status.code() { + Some(0) => (), + a => return Err(GetDeploymentDataError::NixEvalExit(a)), + }; + + let data_json = String::from_utf8(build_output.stdout)?; + + Ok(serde_json::from_str(&data_json)?) +}).try_collect().await +} diff --git a/src/lib.rs b/src/lib.rs index 5cd69f81..e530a8b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,6 +141,7 @@ pub fn init_logger( pub mod settings; pub mod data; +pub mod flake; pub mod deploy; pub mod push; pub mod cli; From 77e28b79adfad7a5e78ecf273b23be9d887af920 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 8 Aug 2021 13:18:41 -0500 Subject: [PATCH 05/10] Refactor homologate data structures --- src/cli.rs | 192 ++++++++++++++---------------------------------- src/data.rs | 96 ++++++++++++------------ src/deploy.rs | 24 +++--- src/push.rs | 16 ++-- src/settings.rs | 18 ++++- 5 files changed, 145 insertions(+), 201 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index c7d68a46..51d2f1d1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -28,66 +28,16 @@ pub struct Opts { /// A list of flakes to deploy alternatively #[clap(long, group = "deploy")] targets: Option>, - /// Check signatures when using `nix copy` - #[clap(short, long)] - checksigs: bool, - /// Use the interactive prompt before deployment - #[clap(short, long)] - interactive: bool, - /// Extra arguments to be passed to nix build - extra_build_args: Vec, - - /// 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, - - /// Keep the build outputs of each built profile - #[clap(short, long)] - keep_result: bool, - /// Location to keep outputs from built profiles in - #[clap(short, long)] - result_path: Option, - /// Skip the automatic pre-build checks - #[clap(short, long)] - skip_checks: bool, - - /// Override the SSH user with the given value - #[clap(long)] - ssh_user: Option, - /// Override the profile user with the given value - #[clap(long)] - profile_user: Option, - /// Override the SSH options used - #[clap(long)] - ssh_opts: Option, - /// Override if the connecting to the target node should be considered fast - #[clap(long)] - fast_connection: Option, - /// Override if a rollback should be attempted if activation fails - #[clap(long)] - auto_rollback: Option, /// Override hostname used for the node #[clap(long)] hostname: Option, - /// Make activation wait for confirmation, or roll back after a period of time - #[clap(long)] - magic_rollback: Option, - /// How long activation should wait for confirmation (if using magic-rollback) - #[clap(long)] - confirm_timeout: Option, - /// Where to store temporary files (only used by magic-rollback) - #[clap(long)] - temp_path: Option, - /// Show what will be activated on the machines - #[clap(long)] - dry_activate: bool, - /// Revoke all previously succeeded deploys when deploying multiple profiles - #[clap(long)] - rollback_succeeded: Option, + + #[clap(flatten)] + flags: data::Flags, + + #[clap(flatten)] + generic_settings: settings::GenericSettings, } /// Returns if the available Nix installation supports flakes @@ -240,27 +190,20 @@ type ToDeploy<'a> = Vec<( )>; async fn run_deploy( - deploy_targets: Vec, - data: Vec, + targets: Vec, + settings: Vec, supports_flakes: bool, - check_sigs: bool, - interactive: bool, - cmd_overrides: &data::CmdOverrides, - keep_result: bool, - result_path: Option<&str>, - extra_build_args: &[String], - debug_logs: bool, - dry_activate: bool, - log_dir: &Option, - rollback_succeeded: bool, + hostname: Option, + cmd_settings: settings::GenericSettings, + cmd_flags: data::Flags, ) -> Result<(), RunDeployError> { - let to_deploy: ToDeploy = deploy_targets + let to_deploy: ToDeploy = targets .iter() - .zip(&data) - .map(|(deploy_target, data)| { - let to_deploys: ToDeploy = match (&deploy_target.node, &deploy_target.profile) { + .zip(&settings) + .map(|(target, root)| { + let to_deploys: ToDeploy = match (&target.node, &target.profile) { (Some(node_name), Some(profile_name)) => { - let node = match data.nodes.get(node_name) { + let node = match root.nodes.get(node_name) { Some(x) => x, None => return Err(RunDeployError::NodeNotFound(node_name.clone())), }; @@ -270,14 +213,14 @@ async fn run_deploy( }; vec![( - deploy_target, - data, + &target, + &root, (node_name.as_str(), node), (profile_name.as_str(), profile), )] } (Some(node_name), None) => { - let node = match data.nodes.get(node_name) { + let node = match root.nodes.get(node_name) { Some(x) => x, None => return Err(RunDeployError::NodeNotFound(node_name.clone())), }; @@ -304,13 +247,13 @@ async fn run_deploy( profiles_list .into_iter() - .map(|x| (deploy_target, data, (node_name.as_str(), node), x)) + .map(|x| (target, root, (node_name.as_str(), node), x)) .collect() } (None, None) => { let mut l = Vec::new(); - for (node_name, node) in &data.nodes { + for (node_name, node) in &root.nodes { let mut profiles_list: Vec<(&str, &settings::Profile)> = Vec::new(); for profile_name in [ @@ -335,7 +278,7 @@ async fn run_deploy( let ll: ToDeploy = profiles_list .into_iter() - .map(|x| (deploy_target, data, (node_name.as_str(), node), x)) + .map(|x| (target, root, (node_name.as_str(), node), x)) .collect(); l.extend(ll); @@ -358,39 +301,39 @@ async fn run_deploy( data::DeployDefs, )> = Vec::new(); - for (deploy_target, data, (node_name, node), (profile_name, profile)) in to_deploy { + for (target, root, (node_name, node), (profile_name, profile)) in to_deploy { let deploy_data = data::make_deploy_data( - &data.generic_settings, + &root.generic_settings, + &cmd_settings, + &cmd_flags, node, node_name, profile, profile_name, - cmd_overrides, - debug_logs, - log_dir.as_deref(), + hostname.as_deref(), ); let deploy_defs = deploy_data.defs()?; - parts.push((deploy_target, deploy_data, deploy_defs)); + parts.push((target, deploy_data, deploy_defs)); } - if interactive { + if cmd_flags.interactive { prompt_deployment(&parts[..])?; } else { print_deployment(&parts[..])?; } - for (deploy_target, deploy_data, deploy_defs) in &parts { + for (target, deploy_data, deploy_defs) in &parts { deploy::push::push_profile(deploy::push::PushProfileData { - supports_flakes, - check_sigs, - repo: &deploy_target.repo, - deploy_data, - deploy_defs, - keep_result, - result_path, - extra_build_args, + supports_flakes: &supports_flakes, + check_sigs: &cmd_flags.checksigs, + repo: &target.repo, + deploy_data: &deploy_data, + deploy_defs: &deploy_defs, + keep_result: &cmd_flags.keep_result, + result_path: cmd_flags.result_path.as_deref(), + extra_build_args: &cmd_flags.extra_build_args, }) .await?; } @@ -402,14 +345,14 @@ async fn run_deploy( // Rollbacks adhere to the global seeting to auto_rollback and secondary // the profile's configuration for (_, deploy_data, deploy_defs) in &parts { - if let Err(e) = deploy::deploy::deploy_profile(deploy_data, deploy_defs, dry_activate).await + if let Err(e) = deploy::deploy::deploy_profile(deploy_data, deploy_defs, cmd_flags.dry_activate).await { error!("{}", e); - if dry_activate { + if cmd_flags.dry_activate { info!("dry run, not rolling back"); } info!("Revoking previous deploys"); - if rollback_succeeded && cmd_overrides.auto_rollback.unwrap_or(true) { + if cmd_flags.rollback_succeeded && cmd_settings.auto_rollback.unwrap_or(true) { // revoking all previous deploys // (adheres to profile configuration if not set explicitely by // the command line) @@ -454,8 +397,8 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { }; deploy::init_logger( - opts.debug_logs, - opts.log_dir.as_deref(), + opts.flags.debug_logs, + opts.flags.log_dir.as_deref(), &deploy::LoggerType::Deploy, )?; @@ -464,51 +407,30 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { .targets .unwrap_or_else(|| vec![opts.clone().target.unwrap_or_else(|| ".".to_string())]); - let deploy_targets: Vec = deploys - .iter() - .map(|f| f.parse::()) - .collect::, data::ParseTargetError>>()?; - - let cmd_overrides = data::CmdOverrides { - ssh_user: opts.ssh_user, - profile_user: opts.profile_user, - ssh_opts: opts.ssh_opts, - fast_connection: opts.fast_connection, - auto_rollback: opts.auto_rollback, - hostname: opts.hostname, - magic_rollback: opts.magic_rollback, - temp_path: opts.temp_path, - confirm_timeout: opts.confirm_timeout, - dry_activate: opts.dry_activate, - }; - 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"); } - if !opts.skip_checks { - for deploy_target in deploy_targets.iter() { - flake::check_deployment(supports_flakes, &deploy_target.repo, &opts.extra_build_args).await?; + let targets: Vec = deploys + .into_iter() + .map(|f| f.parse::()) + .collect::, data::ParseTargetError>>()?; + + if !opts.flags.skip_checks { + for target in targets.iter() { + flake::check_deployment(supports_flakes, &target.repo, &opts.flags.extra_build_args).await?; } } - let result_path = opts.result_path.as_deref(); - let data = flake::get_deployment_data(supports_flakes, &deploy_targets, &opts.extra_build_args).await?; + let settings = flake::get_deployment_data(supports_flakes, &targets, &opts.flags.extra_build_args).await?; run_deploy( - deploy_targets, - data, + targets, + settings, supports_flakes, - opts.checksigs, - opts.interactive, - &cmd_overrides, - opts.keep_result, - result_path, - &opts.extra_build_args, - opts.debug_logs, - opts.dry_activate, - &opts.log_dir, - opts.rollback_succeeded.unwrap_or(true), + opts.hostname, + opts.generic_settings, + opts.flags, ) .await?; diff --git a/src/data.rs b/src/data.rs index 9de663ef..456a9b9a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -6,6 +6,7 @@ use rnix::{types::*, SyntaxKind::*}; use merge::Merge; use thiserror::Error; +use clap::Clap; use crate::settings; @@ -158,33 +159,53 @@ fn test_deploy_target_from_str() { ); } -#[derive(Debug)] -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 magic_rollback: Option, - pub temp_path: Option, - pub confirm_timeout: Option, - pub dry_activate: bool, -} - #[derive(Debug, Clone)] pub struct DeployData<'a> { pub node_name: &'a str, pub node: &'a settings::Node, pub profile_name: &'a str, pub profile: &'a settings::Profile, + pub hostname: Option<&'a str>, - pub cmd_overrides: &'a CmdOverrides, - + pub flags: &'a Flags, pub merged_settings: settings::GenericSettings, +} - pub debug_logs: bool, - pub log_dir: Option<&'a str>, +#[derive(Clap, Debug, Clone)] +pub struct Flags { + /// Check signatures when using `nix copy` + #[clap(short, long)] + pub checksigs: bool, + /// Use the interactive prompt before deployment + #[clap(short, long)] + pub interactive: bool, + /// Extra arguments to be passed to nix build + pub extra_build_args: Vec, + + /// Print debug logs to output + #[clap(short, long)] + pub debug_logs: bool, + /// Directory to print logs to (including the background activation process) + #[clap(long)] + pub log_dir: Option, + + /// Keep the build outputs of each built profile + #[clap(short, long)] + pub keep_result: bool, + /// Location to keep outputs from built profiles in + #[clap(short, long)] + pub result_path: Option, + + /// Skip the automatic pre-build checks + #[clap(short, long)] + pub skip_checks: bool, + /// Make activation wait for confirmation, or roll back after a period of time + /// Show what will be activated on the machines + #[clap(long)] + pub dry_activate: bool, + /// Revoke all previously succeeded deploys when deploying multiple profiles + #[clap(long)] + pub rollback_succeeded: bool, } #[derive(Debug)] @@ -257,47 +278,32 @@ impl<'a> DeployData<'a> { } } -pub fn make_deploy_data<'a, 's>( - top_settings: &'s settings::GenericSettings, +pub fn make_deploy_data<'a>( + top_settings: &'a settings::GenericSettings, + cmd_settings: &'a settings::GenericSettings, + flags: &'a Flags, node: &'a settings::Node, node_name: &'a str, profile: &'a settings::Profile, profile_name: &'a str, - cmd_overrides: &'a CmdOverrides, - debug_logs: bool, - log_dir: Option<&'a str>, + hostname: Option<&'a str>, ) -> DeployData<'a> { - let mut merged_settings = profile.generic_settings.clone(); + let mut merged_settings = cmd_settings.clone(); + merged_settings.merge(profile.generic_settings.clone()); merged_settings.merge(node.generic_settings.clone()); merged_settings.merge(top_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 = Some(fast_connection); - } - if let Some(auto_rollback) = cmd_overrides.auto_rollback { - merged_settings.auto_rollback = Some(auto_rollback); - } - if let Some(magic_rollback) = cmd_overrides.magic_rollback { - merged_settings.magic_rollback = Some(magic_rollback); - } + // if let Some(ref ssh_opts) = cmd_overrides.ssh_opts { + // merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect(); + // } DeployData { node_name, node, profile_name, profile, - cmd_overrides, + hostname, + flags, merged_settings, - debug_logs, - log_dir, } } diff --git a/src/deploy.rs b/src/deploy.rs index 7c1048ea..906ff4e0 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -297,16 +297,16 @@ pub async fn deploy_profile( temp_path: &temp_path, confirm_timeout, magic_rollback, - debug_logs: deploy_data.debug_logs, - log_dir: deploy_data.log_dir, + debug_logs: deploy_data.flags.debug_logs, + log_dir: deploy_data.flags.log_dir.as_deref(), dry_activate, }); 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 hostname = match deploy_data.hostname { + Some(x) => x, + None => deploy_data.node.node_settings.hostname.as_str(), }; let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); @@ -340,8 +340,8 @@ pub async fn deploy_profile( 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_logs: deploy_data.flags.debug_logs, + log_dir: deploy_data.flags.log_dir.as_deref(), }); debug!("Constructed wait command: {}", self_wait_command); @@ -425,15 +425,15 @@ pub async fn revoke( sudo: &deploy_defs.sudo, closure: &deploy_data.profile.profile_settings.path, profile_path: &deploy_data.get_profile_path()?, - debug_logs: deploy_data.debug_logs, - log_dir: deploy_data.log_dir, + debug_logs: deploy_data.flags.debug_logs, + log_dir: deploy_data.flags.log_dir.as_deref(), }); debug!("Constructed revoke command: {}", self_revoke_command); - let hostname = match deploy_data.cmd_overrides.hostname { - Some(ref x) => x, - None => &deploy_data.node.node_settings.hostname, + let hostname = match deploy_data.hostname { + Some(x) => x, + None => deploy_data.node.node_settings.hostname.as_str(), }; let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); diff --git a/src/push.rs b/src/push.rs index ee55a123..d7966d27 100644 --- a/src/push.rs +++ b/src/push.rs @@ -46,12 +46,12 @@ pub enum PushProfileError { } pub struct PushProfileData<'a> { - pub supports_flakes: bool, - pub check_sigs: bool, + pub supports_flakes: &'a bool, + pub check_sigs: &'a bool, pub repo: &'a str, pub deploy_data: &'a data::DeployData<'a>, pub deploy_defs: &'a data::DeployDefs, - pub keep_result: bool, + pub keep_result: &'a bool, pub result_path: Option<&'a str>, pub extra_build_args: &'a [String], } @@ -95,13 +95,13 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr data.deploy_data.profile_name, data.deploy_data.node_name ); - let mut build_command = if data.supports_flakes { + let mut build_command = if *data.supports_flakes { Command::new("nix") } else { Command::new("nix-build") }; - if data.supports_flakes { + if *data.supports_flakes { build_command.arg("build").arg(derivation_name) } else { build_command.arg(derivation_name) @@ -208,9 +208,9 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr // .collect::>() .join(" "); - let hostname = match data.deploy_data.cmd_overrides.hostname { - Some(ref x) => x, - None => &data.deploy_data.node.node_settings.hostname, + let hostname = match data.deploy_data.hostname { + Some(x) => x, + None => data.deploy_data.node.node_settings.hostname.as_str(), }; let copy_exit_status = copy_command diff --git a/src/settings.rs b/src/settings.rs index 9ce50a0f..89e2b74a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,15 +2,22 @@ // // SPDX-License-Identifier: MPL-2.0 +use clap::Clap; use merge::Merge; use serde::Deserialize; use std::collections::HashMap; -#[derive(Deserialize, Debug, Clone, Merge)] +#[derive(Clap, Deserialize, Debug, Clone, Merge)] pub struct GenericSettings { + /// Override the SSH user with the given value + #[clap(long)] #[serde(rename(deserialize = "sshUser"))] pub ssh_user: Option, + /// Override the profile user with the given value + #[clap(long = "profile-user")] pub user: Option, + /// Override the SSH options used + #[clap(long, multiple_occurrences(true), multiple_values(true))] #[serde( skip_serializing_if = "Vec::is_empty", default, @@ -18,14 +25,23 @@ pub struct GenericSettings { )] #[merge(strategy = merge::vec::append)] pub ssh_opts: Vec, + /// Override if the connecting to the target node should be considered fast + #[clap(long)] #[serde(rename(deserialize = "fastConnection"))] pub fast_connection: Option, + /// Override if a rollback should be attempted if activation fails + #[clap(long)] #[serde(rename(deserialize = "autoRollback"))] pub auto_rollback: Option, + /// How long activation should wait for confirmation (if using magic-rollback) + #[clap(long)] #[serde(rename(deserialize = "confirmTimeout"))] pub confirm_timeout: Option, + /// Where to store temporary files (only used by magic-rollback) + #[clap(long)] #[serde(rename(deserialize = "tempPath"))] pub temp_path: Option, + #[clap(long)] #[serde(rename(deserialize = "magicRollback"))] pub magic_rollback: Option, } From 106ef00d6a82c4e2a520b6491218f71f7e70d89a Mon Sep 17 00:00:00 2001 From: David Arnold Date: Thu, 26 Aug 2021 16:55:55 -0500 Subject: [PATCH 06/10] Refactor accesor for ssh uri from DeployData --- src/data.rs | 35 +++++++++++++++++++++++++++++++++++ src/deploy.rs | 51 +++++++++++++++------------------------------------ src/push.rs | 25 ++++++++----------------- 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/src/data.rs b/src/data.rs index 456a9b9a..de5393eb 100644 --- a/src/data.rs +++ b/src/data.rs @@ -220,6 +220,8 @@ pub struct DeployDefs { pub enum DeployDataDefsError { #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")] NoProfileUser(String, String), + #[error("Value `hostname` is not define for profile {0} of node {1}")] + NoProfileHost(String, String), } impl<'a> DeployData<'a> { @@ -246,6 +248,39 @@ impl<'a> DeployData<'a> { }) } + pub fn ssh_uri(&'a self) -> Result { + + let hostname = match self.hostname { + Some(x) => x, + None => &self.node.node_settings.hostname, + }; + let curr_user = &whoami::username(); + let ssh_user = match self.merged_settings.ssh_user { + Some(ref u) => u, + None => curr_user, + }; + Ok(format!("ssh://{}@{}", ssh_user, hostname)) + } + + // can be dropped once ssh fully supports ipv6 uris + pub fn ssh_non_uri(&'a self) -> Result { + + let hostname = match self.hostname { + Some(x) => x, + None => &self.node.node_settings.hostname, + }; + let curr_user = &whoami::username(); + let ssh_user = match self.merged_settings.ssh_user { + Some(ref u) => u, + None => curr_user, + }; + Ok(format!("{}@{}", ssh_user, hostname)) + } + + pub fn ssh_opts(&'a self) -> impl Iterator { + self.merged_settings.ssh_opts.iter() + } + pub fn get_profile_path(&'a self) -> Result { let profile_user = self.get_profile_user()?; let profile_path = match self.profile.profile_settings.profile_path { diff --git a/src/deploy.rs b/src/deploy.rs index 906ff4e0..386cfbfd 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -204,20 +204,19 @@ pub enum ConfirmProfileError { "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}" )] SSHConfirmExit(Option), + + #[error("Deployment data invalid: {0}")] + InvalidDeployDataDefs(#[from] data::DeployDataDefsError), } pub async fn confirm_profile( deploy_data: &data::DeployData<'_>, deploy_defs: &data::DeployDefs, temp_path: Cow<'_, str>, - ssh_addr: &str, ) -> Result<(), ConfirmProfileError> { let mut ssh_confirm_command = Command::new("ssh"); - ssh_confirm_command.arg(ssh_addr); - - for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_confirm_command.arg(ssh_opt); - } + ssh_confirm_command.arg(deploy_data.ssh_non_uri()?); + ssh_confirm_command.args(deploy_data.ssh_opts()); let lock_path = super::make_lock_path(&temp_path, &deploy_data.profile.profile_settings.path); @@ -264,6 +263,9 @@ pub enum DeployProfileError { #[error("Error confirming deployment: {0}")] Confirm(#[from] ConfirmProfileError), + + #[error("Deployment data invalid: {0}")] + InvalidDeployDataDefs(#[from] data::DeployDataDefsError), } pub async fn deploy_profile( @@ -304,19 +306,9 @@ pub async fn deploy_profile( debug!("Constructed activation command: {}", self_activate_command); - let hostname = match deploy_data.hostname { - Some(x) => x, - None => deploy_data.node.node_settings.hostname.as_str(), - }; - - let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); - let mut ssh_activate_command = Command::new("ssh"); - ssh_activate_command.arg(&ssh_addr); - - for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_activate_command.arg(&ssh_opt); - } + ssh_activate_command.arg(deploy_data.ssh_non_uri()?); + ssh_activate_command.args(deploy_data.ssh_opts()); if !magic_rollback || dry_activate { let ssh_activate_exit_status = ssh_activate_command @@ -354,11 +346,8 @@ pub async fn deploy_profile( info!("Creating activation waiter"); let mut ssh_wait_command = Command::new("ssh"); - ssh_wait_command.arg(&ssh_addr); - - for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_wait_command.arg(ssh_opt); - } + ssh_wait_command.arg(deploy_data.ssh_non_uri()?); + ssh_wait_command.args(deploy_data.ssh_opts()); let (send_activate, recv_activate) = tokio::sync::oneshot::channel(); let (send_activated, recv_activated) = tokio::sync::oneshot::channel(); @@ -396,7 +385,7 @@ pub async fn deploy_profile( info!("Success activating, attempting to confirm activation"); - let c = confirm_profile(deploy_data, deploy_defs, temp_path, &ssh_addr).await; + let c = confirm_profile(deploy_data, deploy_defs, temp_path).await; recv_activated.await.unwrap(); c?; } @@ -431,19 +420,9 @@ pub async fn revoke( debug!("Constructed revoke command: {}", self_revoke_command); - let hostname = match deploy_data.hostname { - Some(x) => x, - None => deploy_data.node.node_settings.hostname.as_str(), - }; - - let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); - let mut ssh_activate_command = Command::new("ssh"); - ssh_activate_command.arg(&ssh_addr); - - for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_activate_command.arg(&ssh_opt); - } + ssh_activate_command.arg(deploy_data.ssh_non_uri()?); + ssh_activate_command.args(deploy_data.ssh_opts()); let ssh_revoke = ssh_activate_command .arg(self_revoke_command) diff --git a/src/push.rs b/src/push.rs index d7966d27..a5f53528 100644 --- a/src/push.rs +++ b/src/push.rs @@ -43,6 +43,9 @@ pub enum PushProfileError { Copy(std::io::Error), #[error("Nix copy command resulted in a bad exit code: {0:?}")] CopyExit(Option), + + #[error("Deployment data invalid: {0}")] + InvalidDeployDataDefs(#[from] data::DeployDataDefsError), } pub struct PushProfileData<'a> { @@ -198,26 +201,14 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr copy_command.arg("--no-check-sigs"); } - 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 - // .iter() - // .map(|x| format!("'{}'", x)) - // .collect::>() - .join(" "); - - let hostname = match data.deploy_data.hostname { - Some(x) => x, - None => data.deploy_data.node.node_settings.hostname.as_str(), - }; - let copy_exit_status = copy_command .arg("--to") - .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname)) + .arg(data.deploy_data.ssh_uri()?) .arg(&data.deploy_data.profile.profile_settings.path) - .env("NIX_SSHOPTS", ssh_opts_str) + .env( + "NIX_SSHOPTS", + data.deploy_data.ssh_opts().fold("".to_string(), |s, o| format!("{} {}", s, o)) + ) .status() .await .map_err(PushProfileError::Copy)?; From 9449c7b3a9017b06ba82005c324acb98de7931b5 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 8 Aug 2021 15:09:32 -0500 Subject: [PATCH 07/10] Refactor & simplify the target setting resolver --- Cargo.lock | 18 ++++++ Cargo.toml | 1 + src/cli.rs | 158 ++++++++---------------------------------------- src/data.rs | 171 ++++++++++++++++++++++++++++++++++++++++------------ 4 files changed, 177 insertions(+), 171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08ac3bfc..c9f86dcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" version = "0.7.15" @@ -139,6 +141,7 @@ dependencies = [ "flexi_logger", "fork", "futures-util", + "linked_hash_set", "log", "merge", "notify", @@ -381,6 +384,21 @@ version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "linked_hash_set" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47186c6da4d81ca383c7c47c1bfc80f4b95f4720514d860a5407aaf4233f9588" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "lock_api" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 0ded1259..d3139baa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ clap = "3.0.0-beta.2" flexi_logger = "0.16" fork = "0.1" futures-util = "0.3.6" +linked_hash_set = "0.1.4" log = "0.4" merge = "0.1.0" notify = "5.0.0-pre.3" diff --git a/src/cli.rs b/src/cli.rs index 51d2f1d1..37493dda 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -67,14 +67,13 @@ struct PromptPart<'a> { fn print_deployment( parts: &[( - &data::Target, - data::DeployData, + &data::DeployData, data::DeployDefs, )], ) -> Result<(), toml::ser::Error> { let mut part_map: HashMap> = HashMap::new(); - for (_, data, defs) in parts { + for (data, defs) in parts { part_map .entry(data.node_name.to_string()) .or_insert_with(HashMap::new) @@ -110,8 +109,7 @@ pub enum PromptDeploymentError { fn prompt_deployment( parts: &[( - &data::Target, - data::DeployData, + &data::DeployData, data::DeployDefs, )], ) -> Result<(), PromptDeploymentError> { @@ -166,12 +164,9 @@ pub enum RunDeployError { DeployProfile(#[from] deploy::deploy::DeployProfileError), #[error("Failed to push profile: {0}")] PushProfile(#[from] deploy::push::PushProfileError), - #[error("No profile named `{0}` was found")] - ProfileNotFound(String), - #[error("No node named `{0}` was found")] - NodeNotFound(String), - #[error("Profile was provided without a node name")] - ProfileWithoutNode, + #[error("Failed to resolve target: {0}")] + ResolveTarget(#[from] data::ResolveTargetError), + #[error("Error processing deployment definitions: {0}")] InvalidDeployDataDefs(#[from] data::DeployDataDefsError), #[error("Failed to make printable TOML of deployment: {0}")] @@ -182,13 +177,6 @@ pub enum RunDeployError { RevokeProfile(#[from] deploy::deploy::RevokeProfileError), } -type ToDeploy<'a> = Vec<( - &'a data::Target, - &'a settings::Root, - (&'a str, &'a settings::Node), - (&'a str, &'a settings::Profile), -)>; - async fn run_deploy( targets: Vec, settings: Vec, @@ -197,125 +185,27 @@ async fn run_deploy( cmd_settings: settings::GenericSettings, cmd_flags: data::Flags, ) -> Result<(), RunDeployError> { - let to_deploy: ToDeploy = targets - .iter() - .zip(&settings) - .map(|(target, root)| { - let to_deploys: ToDeploy = match (&target.node, &target.profile) { - (Some(node_name), Some(profile_name)) => { - let node = match root.nodes.get(node_name) { - Some(x) => x, - None => return Err(RunDeployError::NodeNotFound(node_name.clone())), - }; - let profile = match node.node_settings.profiles.get(profile_name) { - Some(x) => x, - None => return Err(RunDeployError::ProfileNotFound(profile_name.clone())), - }; - - vec![( - &target, - &root, - (node_name.as_str(), node), - (profile_name.as_str(), profile), - )] - } - (Some(node_name), None) => { - let node = match root.nodes.get(node_name) { - Some(x) => x, - None => return Err(RunDeployError::NodeNotFound(node_name.clone())), - }; - - let mut profiles_list: Vec<(&str, &settings::Profile)> = Vec::new(); - - for profile_name in [ - node.node_settings.profiles_order.iter().collect(), - node.node_settings.profiles.keys().collect::>(), - ] - .concat() - { - let profile = match node.node_settings.profiles.get(profile_name) { - Some(x) => x, - None => { - return Err(RunDeployError::ProfileNotFound(profile_name.clone())) - } - }; - - if !profiles_list.iter().any(|(n, _)| n == profile_name) { - profiles_list.push((profile_name, profile)); - } - } - - profiles_list - .into_iter() - .map(|x| (target, root, (node_name.as_str(), node), x)) - .collect() - } - (None, None) => { - let mut l = Vec::new(); - - for (node_name, node) in &root.nodes { - let mut profiles_list: Vec<(&str, &settings::Profile)> = Vec::new(); - - for profile_name in [ - node.node_settings.profiles_order.iter().collect(), - node.node_settings.profiles.keys().collect::>(), - ] - .concat() - { - let profile = match node.node_settings.profiles.get(profile_name) { - Some(x) => x, - None => { - return Err(RunDeployError::ProfileNotFound( - profile_name.clone(), - )) - } - }; - - if !profiles_list.iter().any(|(n, _)| n == profile_name) { - profiles_list.push((profile_name, profile)); - } - } - - let ll: ToDeploy = profiles_list - .into_iter() - .map(|x| (target, root, (node_name.as_str(), node), x)) - .collect(); - - l.extend(ll); - } - - l - } - (None, Some(_)) => return Err(RunDeployError::ProfileWithoutNode), - }; - Ok(to_deploys) - }) - .collect::, RunDeployError>>()? - .into_iter() - .flatten() - .collect(); + let deploy_datas_ = targets.into_iter().zip(&settings) + .map( + |(target, root)| + target.resolve( + &root, + &cmd_settings, + &cmd_flags, + hostname.as_deref(), + ) + ) + .collect::>>, data::ResolveTargetError>>()?; + let deploy_datas: Vec<&data::DeployData<'_>> = deploy_datas_.iter().flatten().collect(); let mut parts: Vec<( - &data::Target, - data::DeployData, + &data::DeployData, data::DeployDefs, )> = Vec::new(); - for (target, root, (node_name, node), (profile_name, profile)) in to_deploy { - let deploy_data = data::make_deploy_data( - &root.generic_settings, - &cmd_settings, - &cmd_flags, - node, - node_name, - profile, - profile_name, - hostname.as_deref(), - ); - + for deploy_data in deploy_datas { let deploy_defs = deploy_data.defs()?; - - parts.push((target, deploy_data, deploy_defs)); + parts.push((deploy_data, deploy_defs)); } if cmd_flags.interactive { @@ -324,11 +214,11 @@ async fn run_deploy( print_deployment(&parts[..])?; } - for (target, deploy_data, deploy_defs) in &parts { + for (deploy_data, deploy_defs) in &parts { deploy::push::push_profile(deploy::push::PushProfileData { supports_flakes: &supports_flakes, check_sigs: &cmd_flags.checksigs, - repo: &target.repo, + repo: &deploy_data.repo, deploy_data: &deploy_data, deploy_defs: &deploy_defs, keep_result: &cmd_flags.keep_result, @@ -344,7 +234,7 @@ async fn run_deploy( // In case of an error rollback any previoulsy made deployment. // Rollbacks adhere to the global seeting to auto_rollback and secondary // the profile's configuration - for (_, deploy_data, deploy_defs) in &parts { + for (deploy_data, deploy_defs) in &parts { if let Err(e) = deploy::deploy::deploy_profile(deploy_data, deploy_defs, cmd_flags.dry_activate).await { error!("{}", e); diff --git a/src/data.rs b/src/data.rs index de5393eb..82595880 100644 --- a/src/data.rs +++ b/src/data.rs @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: MPL-2.0 +use linked_hash_set::LinkedHashSet; use rnix::{types::*, SyntaxKind::*}; use merge::Merge; use thiserror::Error; @@ -24,6 +25,98 @@ pub enum ParseTargetError { #[error("Unrecognized node or token encountered")] Unrecognized, } + +#[derive(Error, Debug)] +pub enum ResolveTargetError { + #[error("No node named `{0}` was found in repo `{1}`")] + NodeNotFound(String, String), + #[error("No profile named `{0}` was on node `{1}` found in repo `{2}`")] + ProfileNotFound(String, String, String), + #[error("Profile was provided without a node name for repo `{0}`")] + ProfileWithoutNode(String), +} + +impl<'a> Target { + pub fn resolve( + self, + r: &'a settings::Root, + cs: &'a settings::GenericSettings, + cf: &'a Flags, + hostname: Option<&'a str>, + ) -> Result>, ResolveTargetError> { + match self { + Target{repo, node: Some(node), profile} => { + let node_ = match r.nodes.get(&node) { + Some(x) => x, + None => return Err(ResolveTargetError::NodeNotFound( + node.to_owned(), repo.to_owned() + )), + }; + if let Some(profile) = profile { + let profile_ = match node_.node_settings.profiles.get(&profile) { + Some(x) => x, + None => return Err(ResolveTargetError::ProfileNotFound( + profile.to_owned(), node.to_owned(), repo.to_owned() + )), + }; + Ok({ + let d = DeployData::new( + repo.to_owned(), + node.to_owned(), + profile.to_owned(), + &r.generic_settings, + cs, + cf, + node_, + profile_, + hostname, + ); + vec![d] + }) + } else { + let ordered_profile_names: LinkedHashSet:: = node_.node_settings.profiles_order.iter().cloned().collect(); + let profile_names: LinkedHashSet:: = node_.node_settings.profiles.keys().cloned().collect(); + let prioritized_profile_names: LinkedHashSet::<&String> = ordered_profile_names.union(&profile_names).collect(); + Ok( + prioritized_profile_names + .iter() + .map( + |p| + Target{repo: repo.to_owned(), node: Some(node.to_owned()), profile: Some(p.to_string())}.resolve( + r, cs, cf, hostname, + ) + ) + .collect::>>, ResolveTargetError>>()? + .into_iter().flatten().collect::>>() + ) + } + }, + Target{repo, node: None, profile: None} => { + if let Some(hostname) = hostname { + todo!() // create issue to discuss: + // if allowed, it would be really awkward + // to override the hostname for a series of nodes at once + } + Ok( + r.nodes + .iter() + .map( + |(n, _)| + Target{repo: repo.to_owned(), node: Some(n.to_string()), profile: None}.resolve( + r, cs, cf, hostname, + ) + ) + .collect::>>, ResolveTargetError>>()? + .into_iter().flatten().collect::>>() + ) + }, + Target{repo, node: None, profile: Some(_)} => return Err(ResolveTargetError::ProfileWithoutNode( + repo.to_owned() + )) + } + } +} + impl std::str::FromStr for Target { type Err = ParseTargetError; @@ -44,7 +137,7 @@ impl std::str::FromStr for Target { Some(x) => x, None => { return Ok(Target { - repo, + repo: repo.to_owned(), node: None, profile: None, }) @@ -54,7 +147,7 @@ impl std::str::FromStr for Target { let mut node_over = false; for entry in first_child.children_with_tokens() { - let x: Option = match (entry.kind(), node_over) { + let x = match (entry.kind(), node_over) { (TOKEN_DOT, false) => { node_over = true; None @@ -86,9 +179,9 @@ impl std::str::FromStr for Target { } Ok(Target { - repo, - node, - profile, + repo: repo.to_owned(), + node: node, + profile: profile, }) } } @@ -161,9 +254,10 @@ fn test_deploy_target_from_str() { #[derive(Debug, Clone)] pub struct DeployData<'a> { - pub node_name: &'a str, + pub repo: String, + pub node_name: String, + pub profile_name: String, pub node: &'a settings::Node, - pub profile_name: &'a str, pub profile: &'a settings::Profile, pub hostname: Option<&'a str>, @@ -225,6 +319,39 @@ pub enum DeployDataDefsError { } impl<'a> DeployData<'a> { + + fn new( + repo: String, + node_name: String, + profile_name: String, + top_settings: &'a settings::GenericSettings, + cmd_settings: &'a settings::GenericSettings, + flags: &'a Flags, + node: &'a settings::Node, + profile: &'a settings::Profile, + hostname: Option<&'a str>, + ) -> DeployData<'a> { + let mut merged_settings = cmd_settings.clone(); + merged_settings.merge(profile.generic_settings.clone()); + merged_settings.merge(node.generic_settings.clone()); + merged_settings.merge(top_settings.clone()); + + // if let Some(ref ssh_opts) = cmd_overrides.ssh_opts { + // merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect(); + // } + + DeployData { + repo, + node_name, + profile_name, + node, + profile, + hostname, + flags, + merged_settings, + } + } + pub fn defs(&'a self) -> Result { let ssh_user = match self.merged_settings.ssh_user { Some(ref u) => u.clone(), @@ -312,33 +439,3 @@ impl<'a> DeployData<'a> { Ok(profile_user) } } - -pub fn make_deploy_data<'a>( - top_settings: &'a settings::GenericSettings, - cmd_settings: &'a settings::GenericSettings, - flags: &'a Flags, - node: &'a settings::Node, - node_name: &'a str, - profile: &'a settings::Profile, - profile_name: &'a str, - hostname: Option<&'a str>, -) -> DeployData<'a> { - let mut merged_settings = cmd_settings.clone(); - merged_settings.merge(profile.generic_settings.clone()); - merged_settings.merge(node.generic_settings.clone()); - merged_settings.merge(top_settings.clone()); - - // if let Some(ref ssh_opts) = cmd_overrides.ssh_opts { - // merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect(); - // } - - DeployData { - node_name, - node, - profile_name, - profile, - hostname, - flags, - merged_settings, - } -} From 4a796d618916155b0a9197e779b99202611539e0 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 11 Aug 2021 14:55:39 -0500 Subject: [PATCH 08/10] Refactor merge DeployDefs into DeployData (single view) --- src/cli.rs | 2 +- src/data.rs | 28 ++++++++++++++-------------- src/deploy.rs | 6 +++--- src/push.rs | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 37493dda..51a0a1a7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -168,7 +168,7 @@ pub enum RunDeployError { ResolveTarget(#[from] data::ResolveTargetError), #[error("Error processing deployment definitions: {0}")] - InvalidDeployDataDefs(#[from] data::DeployDataDefsError), + DeployData(#[from] data::DeployDataError), #[error("Failed to make printable TOML of deployment: {0}")] TomlFormat(#[from] toml::ser::Error), #[error("{0}")] diff --git a/src/data.rs b/src/data.rs index 82595880..66c9063c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -265,6 +265,14 @@ pub struct DeployData<'a> { pub merged_settings: settings::GenericSettings, } +#[derive(Error, Debug)] +pub enum DeployDataError { + #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")] + NoProfileUser(String, String), + #[error("Value `hostname` is not define for profile {0} of node {1}")] + NoProfileHost(String, String), +} + #[derive(Clap, Debug, Clone)] pub struct Flags { /// Check signatures when using `nix copy` @@ -310,14 +318,6 @@ pub struct DeployDefs { pub sudo: Option, } -#[derive(Error, Debug)] -pub enum DeployDataDefsError { - #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")] - NoProfileUser(String, String), - #[error("Value `hostname` is not define for profile {0} of node {1}")] - NoProfileHost(String, String), -} - impl<'a> DeployData<'a> { fn new( @@ -352,7 +352,7 @@ impl<'a> DeployData<'a> { } } - pub fn defs(&'a self) -> Result { + pub fn defs(&'a self) -> Result { let ssh_user = match self.merged_settings.ssh_user { Some(ref u) => u.clone(), None => whoami::username(), @@ -375,7 +375,7 @@ impl<'a> DeployData<'a> { }) } - pub fn ssh_uri(&'a self) -> Result { + pub fn ssh_uri(&'a self) -> Result { let hostname = match self.hostname { Some(x) => x, @@ -390,7 +390,7 @@ impl<'a> DeployData<'a> { } // can be dropped once ssh fully supports ipv6 uris - pub fn ssh_non_uri(&'a self) -> Result { + pub fn ssh_non_uri(&'a self) -> Result { let hostname = match self.hostname { Some(x) => x, @@ -408,7 +408,7 @@ impl<'a> DeployData<'a> { self.merged_settings.ssh_opts.iter() } - pub fn get_profile_path(&'a self) -> Result { + pub fn get_profile_path(&'a self) -> Result { let profile_user = self.get_profile_user()?; let profile_path = match self.profile.profile_settings.profile_path { None => match &profile_user[..] { @@ -423,13 +423,13 @@ impl<'a> DeployData<'a> { Ok(profile_path) } - pub fn get_profile_user(&'a self) -> Result { + pub fn get_profile_user(&'a self) -> Result { let profile_user = match self.merged_settings.user { Some(ref x) => x.clone(), None => match self.merged_settings.ssh_user { Some(ref x) => x.clone(), None => { - return Err(DeployDataDefsError::NoProfileUser( + return Err(DeployDataError::NoProfileUser( self.profile_name.to_owned(), self.node_name.to_owned(), )) diff --git a/src/deploy.rs b/src/deploy.rs index 386cfbfd..3feb538c 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -206,7 +206,7 @@ pub enum ConfirmProfileError { SSHConfirmExit(Option), #[error("Deployment data invalid: {0}")] - InvalidDeployDataDefs(#[from] data::DeployDataDefsError), + DeployData(#[from] data::DeployDataError), } pub async fn confirm_profile( @@ -265,7 +265,7 @@ pub enum DeployProfileError { Confirm(#[from] ConfirmProfileError), #[error("Deployment data invalid: {0}")] - InvalidDeployDataDefs(#[from] data::DeployDataDefsError), + DeployData(#[from] data::DeployDataError), } pub async fn deploy_profile( @@ -404,7 +404,7 @@ pub enum RevokeProfileError { SSHRevokeExit(Option), #[error("Deployment data invalid: {0}")] - InvalidDeployDataDefs(#[from] data::DeployDataDefsError), + DeployData(#[from] data::DeployDataError), } pub async fn revoke( deploy_data: &data::DeployData<'_>, diff --git a/src/push.rs b/src/push.rs index a5f53528..6c0f0b92 100644 --- a/src/push.rs +++ b/src/push.rs @@ -45,7 +45,7 @@ pub enum PushProfileError { CopyExit(Option), #[error("Deployment data invalid: {0}")] - InvalidDeployDataDefs(#[from] data::DeployDataDefsError), + DeployData(#[from] data::DeployDataError), } pub struct PushProfileData<'a> { From 29fba1efa512a93dc53cc4e95ec39ba298f6097a Mon Sep 17 00:00:00 2001 From: David Arnold Date: Thu, 26 Aug 2021 17:08:17 -0500 Subject: [PATCH 09/10] Refactor & simplify the target setting resolver --- src/cli.rs | 18 ++- src/data.rs | 86 +++++++++-- src/deploy.rs | 390 ++++++++++++++++++++++++++++---------------------- src/push.rs | 2 +- 4 files changed, 303 insertions(+), 193 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 51a0a1a7..b2fcb9f8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -235,7 +235,14 @@ async fn run_deploy( // Rollbacks adhere to the global seeting to auto_rollback and secondary // the profile's configuration for (deploy_data, deploy_defs) in &parts { - if let Err(e) = deploy::deploy::deploy_profile(deploy_data, deploy_defs, cmd_flags.dry_activate).await + if let Err(e) = deploy::deploy::deploy_profile( + &deploy_data.node_name, + &deploy_data.profile_name, + deploy::deploy::SshCommand::from_data(&deploy_data)?, + deploy::deploy::ActivateCommand::from_data(&deploy_data), + deploy::deploy::WaitCommand::from_data(&deploy_data), + deploy::deploy::ConfirmCommand::from_data(&deploy_data), + ).await { error!("{}", e); if cmd_flags.dry_activate { @@ -246,9 +253,14 @@ async fn run_deploy( // revoking all previous deploys // (adheres to profile configuration if not set explicitely by // the command line) - for (deploy_data, deploy_defs) in &succeeded { + for (deploy_data, _) in &succeeded { if deploy_data.merged_settings.auto_rollback.unwrap_or(true) { - deploy::deploy::revoke(*deploy_data, *deploy_defs).await?; + deploy::deploy::revoke( + &deploy_data.node_name, + &deploy_data.profile_name, + deploy::deploy::SshCommand::from_data(&deploy_data)?, + deploy::deploy::RevokeCommand::from_data(&deploy_data), + ).await?; } } } diff --git a/src/data.rs b/src/data.rs index 66c9063c..3dddc702 100644 --- a/src/data.rs +++ b/src/data.rs @@ -34,6 +34,8 @@ pub enum ResolveTargetError { ProfileNotFound(String, String, String), #[error("Profile was provided without a node name for repo `{0}`")] ProfileWithoutNode(String), + #[error("Deployment data invalid: {0}")] + InvalidDeployDataError(#[from] DeployDataError), } impl<'a> Target { @@ -70,7 +72,7 @@ impl<'a> Target { node_, profile_, hostname, - ); + )?; vec![d] }) } else { @@ -257,12 +259,18 @@ pub struct DeployData<'a> { pub repo: String, pub node_name: String, pub profile_name: String, - pub node: &'a settings::Node, - pub profile: &'a settings::Profile, + pub hostname: Option<&'a str>, pub flags: &'a Flags, + pub node: &'a settings::Node, + pub profile: &'a settings::Profile, pub merged_settings: settings::GenericSettings, + + pub ssh_user: String, + pub temp_path: String, + pub profile_path: String, + pub sudo: Option, } #[derive(Error, Debug)] @@ -330,7 +338,7 @@ impl<'a> DeployData<'a> { node: &'a settings::Node, profile: &'a settings::Profile, hostname: Option<&'a str>, - ) -> DeployData<'a> { + ) -> Result, DeployDataError> { let mut merged_settings = cmd_settings.clone(); merged_settings.merge(profile.generic_settings.clone()); merged_settings.merge(node.generic_settings.clone()); @@ -339,17 +347,49 @@ impl<'a> DeployData<'a> { // if let Some(ref ssh_opts) = cmd_overrides.ssh_opts { // merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect(); // } + let temp_path = match merged_settings.temp_path { + Some(ref x) => x.to_owned(), + None => "/tmp".to_string(), + }; + let profile_user = match merged_settings.user { + Some(ref x) => x.to_owned(), + None => match merged_settings.ssh_user { + Some(ref x) => x.to_owned(), + None => { + return Err(DeployDataError::NoProfileUser(profile_name, node_name)) + } + }, + }; + let profile_path = match profile.profile_settings.profile_path { + None => format!("/nix/var/nix/profiles/{}", match &profile_user[..] { + "root" => profile_name.to_owned(), + _ => format!("per-user/{}/{}", profile_user, profile_name), + }), + Some(ref x) => x.to_owned(), + }; + let ssh_user = match merged_settings.ssh_user { + Some(ref u) => u.to_owned(), + None => whoami::username(), + }; + let sudo = match merged_settings.user { + Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), + _ => None, + }; - DeployData { + Ok(DeployData { repo, node_name, profile_name, node, profile, hostname, + ssh_user, + temp_path, + profile_path, + sudo, flags, merged_settings, - } + }) } pub fn defs(&'a self) -> Result { @@ -357,15 +397,9 @@ impl<'a> DeployData<'a> { Some(ref u) => u.clone(), None => whoami::username(), }; - let profile_user = self.get_profile_user()?; - let profile_path = self.get_profile_path()?; - - let sudo: Option = match self.merged_settings.user { - Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), - _ => None, - }; + let sudo = self.sudo()?; Ok(DeployDefs { ssh_user, @@ -375,6 +409,28 @@ impl<'a> DeployData<'a> { }) } + pub fn sudo(&'a self) -> Result, DeployDataError> { + let ssh_user = match self.merged_settings.ssh_user { + Some(ref u) => u.clone(), + None => whoami::username(), + }; + Ok( + match self.merged_settings.user { + Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), + _ => None, + } + ) + } + + pub fn temp_path(&'a self) -> Result { + Ok( + match self.merged_settings.temp_path { + Some(ref x) => x.to_owned(), + None => "/tmp".to_string(), + } + ) + } + pub fn ssh_uri(&'a self) -> Result { let hostname = match self.hostname { @@ -404,8 +460,8 @@ impl<'a> DeployData<'a> { Ok(format!("{}@{}", ssh_user, hostname)) } - pub fn ssh_opts(&'a self) -> impl Iterator { - self.merged_settings.ssh_opts.iter() + pub fn ssh_opts(&'a self) -> Result, DeployDataError> { + Ok(self.merged_settings.ssh_opts.iter()) } pub fn get_profile_path(&'a self) -> Result { diff --git a/src/deploy.rs b/src/deploy.rs index 3feb538c..23c57b21 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -5,18 +5,41 @@ // SPDX-License-Identifier: MPL-2.0 use log::{debug, info}; -use std::borrow::Cow; use thiserror::Error; use tokio::process::Command; use crate::data; -struct ActivateCommandData<'a> { - sudo: &'a Option, +pub struct SshCommand<'a> { + hoststring: String, + opts: &'a Vec, +} + +impl<'a> SshCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Result { + let hostname = match d.hostname { + Some(x) => x, + None => &d.node.node_settings.hostname, + }; + let hoststring = format!("{}@{}", &d.ssh_user, hostname); + let opts = d.merged_settings.ssh_opts.as_ref(); + Ok(SshCommand {hoststring, opts}) + } + + fn build(&self) -> Command { + let mut cmd = Command::new("ssh"); + cmd.arg(&self.hoststring); + cmd.args(self.opts.iter()); + cmd + } +} + +pub struct ActivateCommand<'a> { + sudo: Option<&'a str>, profile_path: &'a str, + temp_path: &'a str, closure: &'a str, auto_rollback: bool, - temp_path: &'a str, confirm_timeout: u16, magic_rollback: bool, debug_logs: bool, @@ -24,49 +47,66 @@ struct ActivateCommandData<'a> { dry_activate: bool, } -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); +impl<'a> ActivateCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + ActivateCommand { + sudo: d.sudo.as_deref(), + profile_path: &d.profile_path, + temp_path: &d.temp_path, + closure: &d.profile.profile_settings.path, + auto_rollback: d.merged_settings.auto_rollback.unwrap_or(true), + confirm_timeout: d.merged_settings.confirm_timeout.unwrap_or(30), + magic_rollback: d.merged_settings.magic_rollback.unwrap_or(true), + debug_logs: d.flags.debug_logs, + log_dir: d.flags.log_dir.as_deref(), + dry_activate: d.flags.dry_activate, + } } - if let Some(log_dir) = data.log_dir { - self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir); - } + fn build(self) -> String { + let mut cmd = format!("{}/activate-rs", self.closure); - self_activate_command = format!( - "{} activate '{}' '{}' --temp-path '{}'", - self_activate_command, data.closure, data.profile_path, data.temp_path - ); + if self.debug_logs { + cmd = format!("{} --debug-logs", cmd); + } - self_activate_command = format!( - "{} --confirm-timeout {}", - self_activate_command, data.confirm_timeout - ); + if let Some(log_dir) = self.log_dir { + cmd = format!("{} --log-dir {}", cmd, log_dir); + } - if data.magic_rollback { - self_activate_command = format!("{} --magic-rollback", self_activate_command); - } + cmd = format!( + "{} activate '{}' '{}' --temp-path '{}'", + cmd, self.closure, self.profile_path, self.temp_path + ); - if data.auto_rollback { - self_activate_command = format!("{} --auto-rollback", self_activate_command); - } + cmd = format!( + "{} --confirm-timeout {}", + cmd, self.confirm_timeout + ); - if data.dry_activate { - self_activate_command = format!("{} --dry-activate", self_activate_command); - } + if self.magic_rollback { + cmd = format!("{} --magic-rollback", cmd); + } - if let Some(sudo_cmd) = &data.sudo { - self_activate_command = format!("{} {}", sudo_cmd, self_activate_command); - } + if self.auto_rollback { + cmd = format!("{} --auto-rollback", cmd); + } + + if self.dry_activate { + cmd = format!("{} --dry-activate", cmd); + } + + if let Some(sudo_cmd) = &self.sudo { + cmd = format!("{} {}", sudo_cmd, cmd); + } - self_activate_command + cmd + } } #[test] fn test_activation_command_builder() { - let sudo = Some("sudo -u test".to_string()); + let sudo = Some("sudo -u test"); let profile_path = "/blah/profiles/test"; let closure = "/nix/store/blah/etc"; let auto_rollback = true; @@ -78,8 +118,8 @@ fn test_activation_command_builder() { let log_dir = Some("/tmp/something.txt"); assert_eq!( - build_activate_command(&ActivateCommandData { - sudo: &sudo, + ActivateCommand { + sudo, profile_path, closure, auto_rollback, @@ -89,113 +129,165 @@ fn test_activation_command_builder() { debug_logs, log_dir, dry_activate - }), + }.build(), "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt activate '/nix/store/blah/etc' '/blah/profiles/test' --temp-path '/tmp' --confirm-timeout 30 --magic-rollback --auto-rollback" .to_string(), ); } -struct WaitCommandData<'a> { - sudo: &'a Option, +pub struct WaitCommand<'a> { + sudo: Option<&'a str>, 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); +impl<'a> WaitCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + WaitCommand { + sudo: d.sudo.as_deref(), + temp_path: &d.temp_path, + closure: &d.profile.profile_settings.path, + debug_logs: d.flags.debug_logs, + log_dir: d.flags.log_dir.as_deref(), + } } - if let Some(log_dir) = data.log_dir { - self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir); - } + fn build(self) -> String { + let mut cmd = format!("{}/activate-rs", self.closure); - self_activate_command = format!( - "{} wait '{}' --temp-path '{}'", - self_activate_command, data.closure, data.temp_path, - ); + if self.debug_logs { + cmd = format!("{} --debug-logs", cmd); + } - if let Some(sudo_cmd) = &data.sudo { - self_activate_command = format!("{} {}", sudo_cmd, self_activate_command); - } + if let Some(log_dir) = self.log_dir { + cmd = format!("{} --log-dir {}", cmd, log_dir); + } + + cmd = format!( + "{} wait '{}' --temp-path '{}'", + cmd, self.closure, self.temp_path, + ); + + if let Some(sudo_cmd) = &self.sudo { + cmd = format!("{} {}", sudo_cmd, cmd); + } - self_activate_command + cmd + } } #[test] fn test_wait_command_builder() { - let sudo = Some("sudo -u test".to_string()); + let sudo = Some("sudo -u test"); 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, + WaitCommand { + sudo, closure, temp_path, debug_logs, log_dir - }), + }.build(), "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt wait '/nix/store/blah/etc' --temp-path '/tmp'" .to_string(), ); } -struct RevokeCommandData<'a> { - sudo: &'a Option, +pub struct RevokeCommand<'a> { + sudo: Option<&'a str>, closure: &'a str, profile_path: &'a str, debug_logs: bool, log_dir: Option<&'a str>, } -fn build_revoke_command(data: &RevokeCommandData) -> String { - let mut self_activate_command = format!("{}/activate-rs", data.closure); - - if data.debug_logs { - self_activate_command = format!("{} --debug-logs", self_activate_command); +impl<'a> RevokeCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + RevokeCommand { + sudo: d.sudo.as_deref(), + profile_path: &d.profile_path, + closure: &d.profile.profile_settings.path, + debug_logs: d.flags.debug_logs, + log_dir: d.flags.log_dir.as_deref(), + } } - if let Some(log_dir) = data.log_dir { - self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir); - } - self_activate_command = format!("{} revoke '{}'", self_activate_command, data.profile_path); + fn build(self) -> String { + let mut cmd = format!("{}/activate-rs", self.closure); - if let Some(sudo_cmd) = &data.sudo { - self_activate_command = format!("{} {}", sudo_cmd, self_activate_command); - } + if self.debug_logs { + cmd = format!("{} --debug-logs", cmd); + } + + if let Some(log_dir) = self.log_dir { + cmd = format!("{} --log-dir {}", cmd, log_dir); + } + + cmd = format!("{} revoke '{}'", cmd, self.profile_path); + + if let Some(sudo_cmd) = &self.sudo { + cmd = format!("{} {}", sudo_cmd, cmd); + } - self_activate_command + cmd + } } #[test] fn test_revoke_command_builder() { - let sudo = Some("sudo -u test".to_string()); + let sudo = Some("sudo -u test"); let closure = "/nix/store/blah/etc"; let profile_path = "/nix/var/nix/per-user/user/profile"; let debug_logs = true; let log_dir = Some("/tmp/something.txt"); assert_eq!( - build_revoke_command(&RevokeCommandData { - sudo: &sudo, + RevokeCommand { + sudo, closure, profile_path, debug_logs, log_dir - }), + }.build(), "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt revoke '/nix/var/nix/per-user/user/profile'" .to_string(), ); } +pub struct ConfirmCommand<'a> { + sudo: Option<&'a str>, + temp_path: &'a str, + closure: &'a str, +} + +impl<'a> ConfirmCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + ConfirmCommand { + sudo: d.sudo.as_deref(), + temp_path: &d.temp_path, + closure: &d.profile.profile_settings.path, + } + } + + + fn build(self) -> String { + let lock_path = super::make_lock_path(&self.temp_path, &self.closure); + + let mut cmd = format!("rm {}", lock_path); + if let Some(sudo_cmd) = &self.sudo { + cmd = format!("{} {}", sudo_cmd, cmd); + } + cmd + } +} + #[derive(Error, Debug)] pub enum ConfirmProfileError { #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")] @@ -204,34 +296,24 @@ pub enum ConfirmProfileError { "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}" )] SSHConfirmExit(Option), - - #[error("Deployment data invalid: {0}")] - DeployData(#[from] data::DeployDataError), } pub async fn confirm_profile( - deploy_data: &data::DeployData<'_>, - deploy_defs: &data::DeployDefs, - temp_path: Cow<'_, str>, + ssh: SshCommand<'_>, + confirm: ConfirmCommand<'_>, ) -> Result<(), ConfirmProfileError> { - let mut ssh_confirm_command = Command::new("ssh"); - ssh_confirm_command.arg(deploy_data.ssh_non_uri()?); - ssh_confirm_command.args(deploy_data.ssh_opts()); - let lock_path = super::make_lock_path(&temp_path, &deploy_data.profile.profile_settings.path); + let mut ssh_confirm_cmd = ssh.build(); - let mut confirm_command = format!("rm {}", lock_path); - if let Some(sudo_cmd) = &deploy_defs.sudo { - confirm_command = format!("{} {}", sudo_cmd, confirm_command); - } + let confirm_cmd = confirm.build(); debug!( "Attempting to run command to confirm deployment: {}", - confirm_command + confirm_cmd ); - let ssh_confirm_exit_status = ssh_confirm_command - .arg(confirm_command) + let ssh_confirm_exit_status = ssh_confirm_cmd + .arg(confirm_cmd) .status() .await .map_err(ConfirmProfileError::SSHConfirm)?; @@ -263,56 +345,31 @@ pub enum DeployProfileError { #[error("Error confirming deployment: {0}")] Confirm(#[from] ConfirmProfileError), - - #[error("Deployment data invalid: {0}")] - DeployData(#[from] data::DeployDataError), } pub async fn deploy_profile( - deploy_data: &data::DeployData<'_>, - deploy_defs: &data::DeployDefs, - dry_activate: bool, + node_name: &str, + profile_name: &str, + ssh: SshCommand<'_>, + activate: ActivateCommand<'_>, + wait: WaitCommand<'_>, + confirm: ConfirmCommand<'_>, ) -> Result<(), DeployProfileError> { - if !dry_activate { - info!( - "Activating profile `{}` for node `{}`", - deploy_data.profile_name, deploy_data.node_name - ); + if !activate.dry_activate { + info!("Activating profile `{}` for node `{}`", profile_name, node_name); } + let dry_activate = &activate.dry_activate.clone(); + let magic_rollback = &activate.magic_rollback.clone(); - let temp_path: Cow = 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 activate_cmd = activate.build(); - 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.flags.debug_logs, - log_dir: deploy_data.flags.log_dir.as_deref(), - dry_activate, - }); + debug!("Constructed activation command: {}", activate_cmd); - debug!("Constructed activation command: {}", self_activate_command); + let mut ssh_activate_cmd = ssh.build(); - let mut ssh_activate_command = Command::new("ssh"); - ssh_activate_command.arg(deploy_data.ssh_non_uri()?); - ssh_activate_command.args(deploy_data.ssh_opts()); - - if !magic_rollback || dry_activate { - let ssh_activate_exit_status = ssh_activate_command - .arg(self_activate_command) + if !*magic_rollback || *dry_activate { + let ssh_activate_exit_status = ssh_activate_cmd + .arg(activate_cmd) .status() .await .map_err(DeployProfileError::SSHActivate)?; @@ -322,32 +379,25 @@ pub async fn deploy_profile( a => return Err(DeployProfileError::SSHActivateExit(a)), }; - if dry_activate { + if *dry_activate { info!("Completed dry-activate!"); } else { 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.flags.debug_logs, - log_dir: deploy_data.flags.log_dir.as_deref(), - }); + let wait_cmd = wait.build(); - debug!("Constructed wait command: {}", self_wait_command); + debug!("Constructed wait command: {}", wait_cmd); - let ssh_activate = ssh_activate_command - .arg(self_activate_command) + let ssh_activate = ssh_activate_cmd + .arg(activate_cmd) .spawn() .map_err(DeployProfileError::SSHSpawnActivate)?; info!("Creating activation waiter"); - let mut ssh_wait_command = Command::new("ssh"); - ssh_wait_command.arg(deploy_data.ssh_non_uri()?); - ssh_wait_command.args(deploy_data.ssh_opts()); + + let mut ssh_wait_cmd = ssh.build(); let (send_activate, recv_activate) = tokio::sync::oneshot::channel(); let (send_activated, recv_activated) = tokio::sync::oneshot::channel(); @@ -370,7 +420,7 @@ pub async fn deploy_profile( send_activated.send(()).unwrap(); }); tokio::select! { - x = ssh_wait_command.arg(self_wait_command).status() => { + x = ssh_wait_cmd.arg(wait_cmd).status() => { debug!("Wait command ended"); match x.map_err(DeployProfileError::SSHWait)?.code() { Some(0) => (), @@ -385,7 +435,7 @@ pub async fn deploy_profile( info!("Success activating, attempting to confirm activation"); - let c = confirm_profile(deploy_data, deploy_defs, temp_path).await; + let c = confirm_profile(ssh, confirm).await; recv_activated.await.unwrap(); c?; } @@ -402,34 +452,26 @@ pub enum RevokeProfileError { SSHRevoke(std::io::Error), #[error("Revoking over SSH resulted in a bad exit code: {0:?}")] SSHRevokeExit(Option), - - #[error("Deployment data invalid: {0}")] - DeployData(#[from] data::DeployDataError), } pub async fn revoke( - deploy_data: &data::DeployData<'_>, - deploy_defs: &data::DeployDefs, + node_name: &str, + profile_name: &str, + ssh: SshCommand<'_>, + revoke: RevokeCommand<'_>, ) -> Result<(), RevokeProfileError> { - let self_revoke_command = build_revoke_command(&RevokeCommandData { - sudo: &deploy_defs.sudo, - closure: &deploy_data.profile.profile_settings.path, - profile_path: &deploy_data.get_profile_path()?, - debug_logs: deploy_data.flags.debug_logs, - log_dir: deploy_data.flags.log_dir.as_deref(), - }); - - debug!("Constructed revoke command: {}", self_revoke_command); - - let mut ssh_activate_command = Command::new("ssh"); - ssh_activate_command.arg(deploy_data.ssh_non_uri()?); - ssh_activate_command.args(deploy_data.ssh_opts()); - - let ssh_revoke = ssh_activate_command - .arg(self_revoke_command) + info!("Revoking profile `{}` for node `{}`", profile_name, node_name); + + let revoke_cmd = revoke.build(); + debug!("Constructed revoke command: {}", revoke_cmd); + + let mut ssh_revoke_cmd = ssh.build(); + + let ssh_revoke_cmd = ssh_revoke_cmd + .arg(revoke_cmd) .spawn() .map_err(RevokeProfileError::SSHSpawnRevoke)?; - let result = ssh_revoke.wait_with_output().await; + let result = ssh_revoke_cmd.wait_with_output().await; match result { Err(x) => Err(RevokeProfileError::SSHRevoke(x)), diff --git a/src/push.rs b/src/push.rs index 6c0f0b92..76d11b92 100644 --- a/src/push.rs +++ b/src/push.rs @@ -207,7 +207,7 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr .arg(&data.deploy_data.profile.profile_settings.path) .env( "NIX_SSHOPTS", - data.deploy_data.ssh_opts().fold("".to_string(), |s, o| format!("{} {}", s, o)) + data.deploy_data.ssh_opts()?.fold("".to_string(), |s, o| format!("{} {}", s, o)) ) .status() .await From 77ecfad84d4f694b6a614dfc17c9140e5c7f8770 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Thu, 12 Aug 2021 09:21:24 -0500 Subject: [PATCH 10/10] Refactor create push.rs views into data.rs owned data & cleanup --- src/cli.rs | 51 ++++------ src/data.rs | 136 +++----------------------- src/deploy.rs | 6 +- src/push.rs | 261 ++++++++++++++++++++++++++++++-------------------- 4 files changed, 193 insertions(+), 261 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index b2fcb9f8..fe8b1fa5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -66,22 +66,19 @@ struct PromptPart<'a> { } fn print_deployment( - parts: &[( - &data::DeployData, - data::DeployDefs, - )], + parts: &[&data::DeployData], ) -> Result<(), toml::ser::Error> { let mut part_map: HashMap> = HashMap::new(); - for (data, defs) in parts { + for data in parts { part_map .entry(data.node_name.to_string()) .or_insert_with(HashMap::new) .insert( data.profile_name.to_string(), PromptPart { - user: &defs.profile_user, - ssh_user: &defs.ssh_user, + user: &data.profile_user, + ssh_user: &data.ssh_user, path: &data.profile.profile_settings.path, hostname: &data.node.node_settings.hostname, ssh_opts: &data.merged_settings.ssh_opts, @@ -108,10 +105,7 @@ pub enum PromptDeploymentError { } fn prompt_deployment( - parts: &[( - &data::DeployData, - data::DeployDefs, - )], + parts: &[&data::DeployData], ) -> Result<(), PromptDeploymentError> { print_deployment(parts)?; @@ -198,14 +192,10 @@ async fn run_deploy( .collect::>>, data::ResolveTargetError>>()?; let deploy_datas: Vec<&data::DeployData<'_>> = deploy_datas_.iter().flatten().collect(); - let mut parts: Vec<( - &data::DeployData, - data::DeployDefs, - )> = Vec::new(); + let mut parts: Vec<&data::DeployData> = Vec::new(); for deploy_data in deploy_datas { - let deploy_defs = deploy_data.defs()?; - parts.push((deploy_data, deploy_defs)); + parts.push(deploy_data); } if cmd_flags.interactive { @@ -214,27 +204,24 @@ async fn run_deploy( print_deployment(&parts[..])?; } - for (deploy_data, deploy_defs) in &parts { - deploy::push::push_profile(deploy::push::PushProfileData { - supports_flakes: &supports_flakes, - check_sigs: &cmd_flags.checksigs, - repo: &deploy_data.repo, - deploy_data: &deploy_data, - deploy_defs: &deploy_defs, - keep_result: &cmd_flags.keep_result, - result_path: cmd_flags.result_path.as_deref(), - extra_build_args: &cmd_flags.extra_build_args, - }) + for deploy_data in &parts { + deploy::push::push_profile( + supports_flakes, + deploy::push::ShowDerivationCommand::from_data(&deploy_data), + deploy::push::BuildCommand::from_data(&deploy_data), + deploy::push::SignCommand::from_data(&deploy_data), + deploy::push::CopyCommand::from_data(&deploy_data), + ) .await?; } - let mut succeeded: Vec<(&data::DeployData, &data::DeployDefs)> = vec![]; + let mut succeeded: Vec<&data::DeployData> = vec![]; // Run all deployments // In case of an error rollback any previoulsy made deployment. // Rollbacks adhere to the global seeting to auto_rollback and secondary // the profile's configuration - for (deploy_data, deploy_defs) in &parts { + for deploy_data in &parts { if let Err(e) = deploy::deploy::deploy_profile( &deploy_data.node_name, &deploy_data.profile_name, @@ -253,7 +240,7 @@ async fn run_deploy( // revoking all previous deploys // (adheres to profile configuration if not set explicitely by // the command line) - for (deploy_data, _) in &succeeded { + for deploy_data in &succeeded { if deploy_data.merged_settings.auto_rollback.unwrap_or(true) { deploy::deploy::revoke( &deploy_data.node_name, @@ -266,7 +253,7 @@ async fn run_deploy( } break; } - succeeded.push((deploy_data, deploy_defs)) + succeeded.push(deploy_data) } Ok(()) diff --git a/src/data.rs b/src/data.rs index 3dddc702..6fd4473a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -260,16 +260,18 @@ pub struct DeployData<'a> { pub node_name: String, pub profile_name: String, - pub hostname: Option<&'a str>, - pub flags: &'a Flags, pub node: &'a settings::Node, pub profile: &'a settings::Profile, pub merged_settings: settings::GenericSettings, + pub hostname: &'a str, + pub ssh_user: String, + pub ssh_uri: String, pub temp_path: String, pub profile_path: String, + pub profile_user: String, pub sudo: Option, } @@ -318,14 +320,6 @@ pub struct Flags { pub rollback_succeeded: bool, } -#[derive(Debug)] -pub struct DeployDefs { - pub ssh_user: String, - pub profile_user: String, - pub profile_path: String, - pub sudo: Option, -} - impl<'a> DeployData<'a> { fn new( @@ -351,14 +345,10 @@ impl<'a> DeployData<'a> { Some(ref x) => x.to_owned(), None => "/tmp".to_string(), }; - let profile_user = match merged_settings.user { - Some(ref x) => x.to_owned(), - None => match merged_settings.ssh_user { - Some(ref x) => x.to_owned(), - None => { - return Err(DeployDataError::NoProfileUser(profile_name, node_name)) - } - }, + let profile_user = if let Some(ref x) = merged_settings.user { x.to_owned() } else { + if let Some(ref x) = merged_settings.ssh_user { x.to_owned() } else { + return Err(DeployDataError::NoProfileUser(profile_name, node_name)) + } }; let profile_path = match profile.profile_settings.profile_path { None => format!("/nix/var/nix/profiles/{}", match &profile_user[..] { @@ -375,6 +365,11 @@ impl<'a> DeployData<'a> { Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), _ => None, }; + let hostname = match hostname { + Some(x) => x, + None => &node.node_settings.hostname, + }; + let ssh_uri = format!("ssh://{}@{}", &ssh_user, &hostname); Ok(DeployData { repo, @@ -384,114 +379,13 @@ impl<'a> DeployData<'a> { profile, hostname, ssh_user, + ssh_uri, temp_path, profile_path, + profile_user, sudo, flags, merged_settings, }) } - - pub fn defs(&'a self) -> Result { - let ssh_user = match self.merged_settings.ssh_user { - Some(ref u) => u.clone(), - None => whoami::username(), - }; - let profile_user = self.get_profile_user()?; - let profile_path = self.get_profile_path()?; - let sudo = self.sudo()?; - - Ok(DeployDefs { - ssh_user, - profile_user, - profile_path, - sudo, - }) - } - - pub fn sudo(&'a self) -> Result, DeployDataError> { - let ssh_user = match self.merged_settings.ssh_user { - Some(ref u) => u.clone(), - None => whoami::username(), - }; - Ok( - match self.merged_settings.user { - Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), - _ => None, - } - ) - } - - pub fn temp_path(&'a self) -> Result { - Ok( - match self.merged_settings.temp_path { - Some(ref x) => x.to_owned(), - None => "/tmp".to_string(), - } - ) - } - - pub fn ssh_uri(&'a self) -> Result { - - let hostname = match self.hostname { - Some(x) => x, - None => &self.node.node_settings.hostname, - }; - let curr_user = &whoami::username(); - let ssh_user = match self.merged_settings.ssh_user { - Some(ref u) => u, - None => curr_user, - }; - Ok(format!("ssh://{}@{}", ssh_user, hostname)) - } - - // can be dropped once ssh fully supports ipv6 uris - pub fn ssh_non_uri(&'a self) -> Result { - - let hostname = match self.hostname { - Some(x) => x, - None => &self.node.node_settings.hostname, - }; - let curr_user = &whoami::username(); - let ssh_user = match self.merged_settings.ssh_user { - Some(ref u) => u, - None => curr_user, - }; - Ok(format!("{}@{}", ssh_user, hostname)) - } - - pub fn ssh_opts(&'a self) -> Result, DeployDataError> { - Ok(self.merged_settings.ssh_opts.iter()) - } - - pub fn get_profile_path(&'a self) -> Result { - let profile_user = self.get_profile_user()?; - let profile_path = match self.profile.profile_settings.profile_path { - None => match &profile_user[..] { - "root" => format!("/nix/var/nix/profiles/{}", self.profile_name), - _ => format!( - "/nix/var/nix/profiles/per-user/{}/{}", - profile_user, self.profile_name - ), - }, - Some(ref x) => x.clone(), - }; - Ok(profile_path) - } - - pub fn get_profile_user(&'a self) -> Result { - let profile_user = match self.merged_settings.user { - Some(ref x) => x.clone(), - None => match self.merged_settings.ssh_user { - Some(ref x) => x.clone(), - None => { - return Err(DeployDataError::NoProfileUser( - self.profile_name.to_owned(), - self.node_name.to_owned(), - )) - } - }, - }; - Ok(profile_user) - } } diff --git a/src/deploy.rs b/src/deploy.rs index 23c57b21..d517d6af 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -17,11 +17,7 @@ pub struct SshCommand<'a> { impl<'a> SshCommand<'a> { pub fn from_data(d: &'a data::DeployData) -> Result { - let hostname = match d.hostname { - Some(x) => x, - None => &d.node.node_settings.hostname, - }; - let hoststring = format!("{}@{}", &d.ssh_user, hostname); + let hoststring = format!("{}@{}", &d.ssh_user, d.hostname); let opts = d.merged_settings.ssh_opts.as_ref(); Ok(SshCommand {hoststring, opts}) } diff --git a/src/push.rs b/src/push.rs index 76d11b92..e1c0d88a 100644 --- a/src/push.rs +++ b/src/push.rs @@ -48,31 +48,158 @@ pub enum PushProfileError { DeployData(#[from] data::DeployDataError), } -pub struct PushProfileData<'a> { - pub supports_flakes: &'a bool, - pub check_sigs: &'a bool, - pub repo: &'a str, - pub deploy_data: &'a data::DeployData<'a>, - pub deploy_defs: &'a data::DeployDefs, - pub keep_result: &'a bool, - pub result_path: Option<&'a str>, - pub extra_build_args: &'a [String], +pub struct ShowDerivationCommand<'a> { + closure: &'a str, } -pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileError> { - debug!( - "Finding the deriver of store path for {}", - &data.deploy_data.profile.profile_settings.path - ); +impl<'a> ShowDerivationCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + ShowDerivationCommand { + closure: d.profile.profile_settings.path.as_str(), + } + } + + fn build(self) -> Command { + // `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :( + let mut cmd = Command::new("nix"); + + cmd + .arg("show-derivation") + .arg(&self.closure); + //cmd.what_is_this; + cmd + } +} + +pub struct SignCommand<'a> { + closure: &'a str, +} + +impl<'a> SignCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + SignCommand { + closure: d.profile.profile_settings.path.as_str(), + } + } + + fn build(self, local_key: String) -> Command { + let mut cmd = Command::new("nix"); + + cmd + .arg("sign-paths") + .arg("-r") + .arg("-k") + .arg(local_key) + .arg(&self.closure); + //cmd.what_is_this; + cmd + } +} + +pub struct CopyCommand<'a> { + closure: &'a str, + fast_connection: bool, + check_sigs: &'a bool, + ssh_uri: &'a str, + ssh_opts: String, +} + +impl<'a> CopyCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + CopyCommand { + closure: d.profile.profile_settings.path.as_str(), + fast_connection: d.merged_settings.fast_connection.unwrap_or(false), + check_sigs: &d.flags.checksigs, + ssh_uri: d.ssh_uri.as_str(), + ssh_opts: d.merged_settings.ssh_opts.iter().fold("".to_string(), |s, o| format!("{} {}", s, o)), + } + } + + fn build(self) -> Command { + let mut cmd = Command::new("nix"); + + cmd.arg("copy"); + + if self.fast_connection { + cmd.arg("--substitute-on-destination"); + } + + if !self.check_sigs { + cmd.arg("--no-check-sigs"); + } + cmd + .arg("--to") + .arg(self.ssh_uri) + .arg(self.closure) + .env("NIX_SSHOPTS", self.ssh_opts); + //cmd.what_is_this; + cmd + } +} + +pub struct BuildCommand<'a> { + node_name: &'a str, + profile_name: &'a str, + keep_result: &'a bool, + result_path: &'a str, + extra_build_args: &'a Vec, +} + +impl<'a> BuildCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + BuildCommand { + node_name: d.node_name.as_str(), + profile_name: d.profile_name.as_str(), + keep_result: &d.flags.keep_result, + result_path: &d.flags.result_path.as_deref().unwrap_or("./.deploy-gc"), + extra_build_args: &d.flags.extra_build_args, + } + } + + fn build(self, derivation_name: &str, supports_flakes: bool) -> Command { + let mut cmd = if supports_flakes { + Command::new("nix") + } else { + Command::new("nix-build") + }; - // `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :( - let mut show_derivation_command = Command::new("nix"); + if supports_flakes { + cmd.arg("build").arg(derivation_name) + } else { + cmd.arg(derivation_name) + }; - show_derivation_command - .arg("show-derivation") - .arg(&data.deploy_data.profile.profile_settings.path); + match (self.keep_result, supports_flakes) { + (true, _) => { + cmd.arg("--out-link").arg(format!( + "{}/{}/{}", + self.result_path, self.node_name, self.profile_name + )) + } + (false, false) => cmd.arg("--no-out-link"), + (false, true) => cmd.arg("--no-link"), + }; + cmd.args(self.extra_build_args.iter()); + // cmd.what_is_this; + cmd + } +} - let show_derivation_output = show_derivation_command +pub async fn push_profile( + supports_flakes: bool, + show_derivation: ShowDerivationCommand<'_>, + build: BuildCommand<'_>, + sign: SignCommand<'_>, + copy: CopyCommand<'_>, +) -> Result<(), PushProfileError> { + let node_name = build.node_name; + let profile_name = build.profile_name; + let closure = show_derivation.closure; + + debug!("Finding the deriver of store path for {}", closure); + let mut show_derivation_cmd = show_derivation.build(); + + let show_derivation_output = show_derivation_cmd .output() .await .map_err(PushProfileError::ShowDerivation)?; @@ -93,41 +220,11 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr .next() .ok_or(PushProfileError::ShowDerivationEmpty)?; - info!( - "Building profile `{}` for node `{}`", - data.deploy_data.profile_name, data.deploy_data.node_name - ); - - let mut build_command = if *data.supports_flakes { - Command::new("nix") - } else { - Command::new("nix-build") - }; - - if *data.supports_flakes { - build_command.arg("build").arg(derivation_name) - } else { - build_command.arg(derivation_name) - }; + info!("Building profile `{}` for node `{}`", profile_name, node_name); - match (data.keep_result, data.supports_flakes) { - (true, _) => { - let result_path = data.result_path.unwrap_or("./.deploy-gc"); + let mut build_cmd = build.build(*derivation_name, supports_flakes); - build_command.arg("--out-link").arg(format!( - "{}/{}/{}", - 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 data.extra_build_args { - build_command.arg(extra_arg); - } - - let build_exit_status = build_command + let build_exit_status = build_cmd // Logging should be in stderr, this just stops the store path from printing for no reason .stdout(Stdio::null()) .status() @@ -139,42 +236,19 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr a => return Err(PushProfileError::BuildExit(a)), }; - if !Path::new( - format!( - "{}/deploy-rs-activate", - data.deploy_data.profile.profile_settings.path - ) - .as_str(), - ) - .exists() - { + if !Path::new(format!("{}/deploy-rs-activate", closure).as_str()).exists() { return Err(PushProfileError::DeployRsActivateDoesntExist); } - if !Path::new( - format!( - "{}/activate-rs", - data.deploy_data.profile.profile_settings.path - ) - .as_str(), - ) - .exists() - { + if !Path::new(format!("{}/activate-rs", closure).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 `{}`", - data.deploy_data.profile_name, data.deploy_data.node_name - ); + info!("Signing key present! Signing profile `{}` for node `{}`", profile_name, node_name); - let sign_exit_status = Command::new("nix") - .arg("sign-paths") - .arg("-r") - .arg("-k") - .arg(local_key) - .arg(&data.deploy_data.profile.profile_settings.path) + let mut sign_cmd = sign.build(local_key); + let sign_exit_status = sign_cmd .status() .await .map_err(PushProfileError::Sign)?; @@ -185,30 +259,11 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr }; } - info!( - "Copying profile `{}` to node `{}`", - data.deploy_data.profile_name, data.deploy_data.node_name - ); - - let mut copy_command = Command::new("nix"); - copy_command.arg("copy"); + info!("Copying profile `{}` to node `{}`", profile_name, node_name); - if data.deploy_data.merged_settings.fast_connection != Some(true) { - copy_command.arg("--substitute-on-destination"); - } - - if !data.check_sigs { - copy_command.arg("--no-check-sigs"); - } + let mut copy_cmd = copy.build(); - let copy_exit_status = copy_command - .arg("--to") - .arg(data.deploy_data.ssh_uri()?) - .arg(&data.deploy_data.profile.profile_settings.path) - .env( - "NIX_SSHOPTS", - data.deploy_data.ssh_opts()?.fold("".to_string(), |s, o| format!("{} {}", s, o)) - ) + let copy_exit_status = copy_cmd .status() .await .map_err(PushProfileError::Copy)?;