From 313a5a8ebe997bfe19494b1174d8e17d80eb3594 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 8 Aug 2021 15:09:32 -0500 Subject: [PATCH] Refactor & simplify the target setting resolver --- Cargo.lock | 18 +++++ Cargo.toml | 1 + src/cli.rs | 157 +++++++----------------------------------- src/data.rs | 189 ++++++++++++++++++++++++++++++++++++++------------- src/flake.rs | 2 +- 5 files changed, 189 insertions(+), 178 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 0e5cc2d8..1ddf8d46 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}")] DeployDataDefs(#[from] data::DeployDataDefsError), #[error("Failed to make printable TOML of deployment: {0}")] @@ -183,141 +178,41 @@ pub enum RunDeployError { } type ToDeploy<'a> = Vec<( - &'a data::Target, + &'a data::Target<'a>, &'a settings::Root, (&'a str, &'a settings::Node), (&'a str, &'a settings::Profile), )>; async fn run_deploy( - targets: Vec, + targets: Vec>, settings: Vec, supports_flakes: bool, hostname: Option, 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 => Err(RunDeployError::NodeNotFound(node_name.to_owned()))?, - }; - let profile = match node.node_settings.profiles.get(profile_name) { - Some(x) => x, - None => Err(RunDeployError::ProfileNotFound(profile_name.to_owned()))?, - }; - - 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.to_owned())), - }; - - 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.to_owned(), - )) - } - }; - - 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.to_owned(), - )) - } - }; - - 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 { @@ -326,11 +221,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, @@ -346,7 +241,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 b314381a..6e37d934 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; @@ -11,10 +12,10 @@ use clap::Clap; use crate::settings; #[derive(PartialEq, Debug)] -pub struct Target { - pub repo: String, - pub node: Option, - pub profile: Option, +pub struct Target<'a> { + pub repo: &'a String, + pub node: Option<&'a String>, + pub profile: Option<&'a String>, } #[derive(Error, Debug)] @@ -24,7 +25,99 @@ pub enum ParseTargetError { #[error("Unrecognized node or token encountered")] Unrecognized, } -impl std::str::FromStr for Target { + +#[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<'a> { + 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 => 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 => Err(ResolveTargetError::ProfileNotFound( + profile.to_owned(), node.to_owned(), repo.to_owned() + ))? + }; + Ok({ + let d = DeployData::new( + repo, + node, + profile, + &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, node: Some(node), 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, node: Some(&n.to_string()), profile: None}.resolve( + r, cs, cf, hostname, + ) + ) + .collect::>>, ResolveTargetError>>()? + .into_iter().flatten().collect::>>() + ) + }, + Target{repo, node: None, profile: Some(_)} => Err(ResolveTargetError::ProfileWithoutNode( + repo.to_owned() + ))? + } + } +} + +impl std::str::FromStr for Target<'_> { type Err = ParseTargetError; fn from_str(s: &str) -> Result { @@ -34,8 +127,8 @@ impl std::str::FromStr for Target { None => (s.to_string(), None), }; - let mut node: Option = None; - let mut profile: Option = None; + let mut node: Option<&String> = None; + let mut profile: Option<&String> = None; if let Some(fragment) = maybe_fragment { let ast = rnix::parse(fragment); @@ -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: Option<&String> = match (entry.kind(), node_over) { (TOKEN_DOT, false) => { node_over = true; None @@ -62,8 +155,8 @@ impl std::str::FromStr for Target { (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_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() @@ -72,7 +165,7 @@ impl std::str::FromStr for Target { .nth(1) .unwrap(); - Some(c.into_token().unwrap().text().to_string()) + Some(&c.into_token().unwrap().text().to_string()) } _ => return Err(ParseTargetError::Unrecognized), }; @@ -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 repo: &'a str, pub node_name: &'a str, - pub node: &'a settings::Node, pub profile_name: &'a str, + pub node: &'a settings::Node, pub profile: &'a settings::Profile, pub hostname: Option<&'a str>, @@ -225,6 +319,39 @@ pub enum DeployDataDefsError { } impl<'a> DeployData<'a> { + + fn new( + repo: &'a str, + node_name: &'a str, + profile_name: &'a str, + 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, - } -} diff --git a/src/flake.rs b/src/flake.rs index cf42da1f..26afb6eb 100644 --- a/src/flake.rs +++ b/src/flake.rs @@ -76,7 +76,7 @@ pub enum GetDeploymentDataError { /// 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], + flakes: &[data::Target<'_>], extra_build_args: &[String], ) -> Result, GetDeploymentDataError> { futures_util::stream::iter(flakes).then(|flake| async move {