diff --git a/src/nix/node_filter.rs b/src/nix/node_filter.rs index 6e459fa..3507a1e 100644 --- a/src/nix/node_filter.rs +++ b/src/nix/node_filter.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::convert::AsRef; -use std::iter::{FromIterator, Iterator}; +use std::iter::Iterator; use std::str::FromStr; use clap::Args; @@ -28,22 +28,26 @@ The list is comma-separated and globs are supported. To match tags, prepend the pub on: Option, } -/// A node filter containing a list of rules. -#[derive(Clone, Debug)] -pub struct NodeFilter { - rules: Vec, -} - /// A filter rule. -/// -/// The filter rules are OR'd together. #[derive(Debug, Clone, Eq, PartialEq)] -enum Rule { +pub enum NodeFilter { /// Matches a node's attribute name. MatchName(GlobPattern), /// Matches a node's `deployment.tags`. MatchTag(GlobPattern), + + /// Matches an Union + Union(Vec>), + + /// Matches an Intersection + Inter(Vec>), + + /// Matches the complementary + Not(Box), + + /// Empty + Empty, } impl FromStr for NodeFilter { @@ -53,7 +57,144 @@ impl FromStr for NodeFilter { } } +fn end_delimiter(c: char) -> bool { + [',', '&', ')'].contains(&c) +} + impl NodeFilter { + fn and(a: Self, b: Self) -> Self { + match (a, b) { + (Self::Inter(mut av), Self::Inter(mut bv)) => { + av.append(&mut bv); + Self::Inter(av) + } + (Self::Inter(mut av), b) => { + av.push(Box::new(b)); + Self::Inter(av) + } + (a, Self::Inter(mut bv)) => { + bv.push(Box::new(a)); + Self::Inter(bv) + } + (a, b) => Self::Inter(vec![Box::new(a), Box::new(b)]), + } + } + + fn or(a: Self, b: Self) -> Self { + match (a, b) { + (Self::Union(mut av), Self::Union(mut bv)) => { + av.append(&mut bv); + Self::Union(av) + } + (Self::Union(mut av), b) => { + av.push(Box::new(b)); + Self::Union(av) + } + (a, Self::Union(mut bv)) => { + bv.push(Box::new(a)); + Self::Union(bv) + } + (a, b) => Self::Union(vec![Box::new(a), Box::new(b)]), + } + } + + fn not(a: Self) -> Self { + if let Self::Not(ae) = a { + *ae + } else { + Self::Not(Box::new(a)) + } + } + + fn parse_expr0(expr: &str) -> ColmenaResult<(Self, &str)> { + // tag, name, !, (e) + let expr = expr.trim_start(); + if let Some(follow) = expr.strip_prefix('!') { + let (e, end) = Self::parse_expr0(follow)?; + Ok((Self::not(e), end)) + } else if let Some(follow) = expr.strip_prefix('(') { + let (e, end) = Self::parse_expr2(follow)?; + // TODO: other error + Ok(( + e, + end.strip_prefix(')').ok_or(ColmenaError::EmptyFilterRule)?, + )) + } else if let Some(follow) = expr.strip_prefix('@') { + match follow + .find(end_delimiter) + .map(|idx| follow.split_at(idx)) + .map(|(tag, end)| (tag.trim_end(), end)) + { + Some((tag, end)) => { + if tag.is_empty() { + return Err(ColmenaError::EmptyFilterRule); + } else { + Ok((Self::MatchTag(GlobPattern::new(tag).unwrap()), end)) + } + } + None => { + if follow.is_empty() { + Err(ColmenaError::EmptyFilterRule) + } else { + Ok((Self::MatchTag(GlobPattern::new(follow).unwrap()), "")) + } + } + } + } else { + match expr + .find(end_delimiter) + .map(|idx| expr.split_at(idx)) + .map(|(tag, end)| (tag.trim_end(), end)) + { + Some((tag, end)) => { + if tag.is_empty() { + Err(ColmenaError::EmptyFilterRule) + } else { + Ok((Self::MatchName(GlobPattern::new(tag).unwrap()), end)) + } + } + None => { + if expr.is_empty() { + Err(ColmenaError::EmptyFilterRule) + } else { + Ok((Self::MatchName(GlobPattern::new(expr).unwrap()), "")) + } + } + } + } + } + + fn parse_op1(beg: Self, expr: &str) -> ColmenaResult<(Self, &str)> { + let expr = expr.trim_start(); + if let Some(follow) = expr.strip_prefix(',') { + let (e, end) = Self::parse_expr0(follow)?; + Self::parse_op1(Self::or(beg, e), end) + } else { + Ok((beg, expr)) + } + } + + fn parse_expr1(expr: &str) -> ColmenaResult<(Self, &str)> { + // , + let (e, follow) = Self::parse_expr0(expr)?; + Self::parse_op1(e, follow) + } + + fn parse_op2(beg: Self, expr: &str) -> ColmenaResult<(Self, &str)> { + if let Some(follow) = expr.strip_prefix('&') { + let (e, end) = Self::parse_expr1(follow)?; + Self::parse_op2(Self::and(beg, e), end) + } else { + Ok((beg, expr)) + } + } + + fn parse_expr2(expr: &str) -> ColmenaResult<(Self, &str)> { + // & + let (e, follow) = Self::parse_expr1(expr)?; + Self::parse_op2(e, follow) + } + /// Creates a new filter using an expression passed using `--on`. pub fn new>(filter: S) -> ColmenaResult { let filter = filter.as_ref(); @@ -62,29 +203,15 @@ impl NodeFilter { if trimmed.is_empty() { log::warn!("Filter \"{}\" is blank and will match nothing", filter); - return Ok(Self { rules: Vec::new() }); + return Ok(Self::Empty); + } + let (e, end) = Self::parse_expr2(trimmed)?; + if end != "" { + // TODO: other error + Err(ColmenaError::EmptyFilterRule) + } else { + Ok(e) } - - let rules = trimmed - .split(',') - .map(|pattern| { - let pattern = pattern.trim(); - - if pattern.is_empty() { - return Err(ColmenaError::EmptyFilterRule); - } - - if let Some(tag_pattern) = pattern.strip_prefix('@') { - Ok(Rule::MatchTag(GlobPattern::new(tag_pattern).unwrap())) - } else { - Ok(Rule::MatchName(GlobPattern::new(pattern).unwrap())) - } - }) - .collect::>>(); - - let rules = Result::from_iter(rules)?; - - Ok(Self { rules }) } /// Returns whether the filter has any rule matching NodeConfig information. @@ -93,7 +220,29 @@ impl NodeFilter { /// especially when its values (e.g., tags) depend on other parts of /// the configuration. pub fn has_node_config_rules(&self) -> bool { - self.rules.iter().any(|rule| rule.matches_node_config()) + match self { + Self::MatchName(_) => false, + Self::MatchTag(_) => true, + Self::Union(v) => v.iter().any(|e| e.has_node_config_rules()), + Self::Inter(v) => v.iter().any(|e| e.has_node_config_rules()), + Self::Not(e) => e.has_node_config_rules(), + Self::Empty => false, + } + } + + fn is_accepted(&self, name: &NodeName, config: Option<&NodeConfig>) -> bool { + match self { + Self::MatchName(pat) => pat.matches(name.as_str()), + Self::MatchTag(pat) => config + .unwrap() + .tags() + .iter() + .any(|tag| pat.matches(tag.as_str())), + Self::Union(v) => v.iter().any(|e| e.is_accepted(name, config)), + Self::Inter(v) => v.iter().all(|e| e.is_accepted(name, config)), + Self::Not(e) => !e.is_accepted(name, config), + Self::Empty => false, + } } /// Runs the filter against a set of NodeConfigs and returns the matched ones. @@ -101,30 +250,17 @@ impl NodeFilter { where I: Iterator, { - if self.rules.is_empty() { + if self == &Self::Empty { return HashSet::new(); } nodes .filter_map(|(name, node)| { - for rule in self.rules.iter() { - match rule { - Rule::MatchName(pat) => { - if pat.matches(name.as_str()) { - return Some(name); - } - } - Rule::MatchTag(pat) => { - for tag in node.tags() { - if pat.matches(tag) { - return Some(name); - } - } - } - } + if self.is_accepted(name, Some(node)) { + Some(name) + } else { + None } - - None }) .cloned() .collect() @@ -132,32 +268,24 @@ impl NodeFilter { /// Runs the filter against a set of node names and returns the matched ones. pub fn filter_node_names(&self, nodes: &[NodeName]) -> ColmenaResult> { - nodes.iter().filter_map(|name| -> Option> { - for rule in self.rules.iter() { - match rule { - Rule::MatchName(pat) => { - if pat.matches(name.as_str()) { - return Some(Ok(name.clone())); - } + if self.has_node_config_rules() { + Err(ColmenaError::Unknown { + message: format!( + "Not enough information to run rule {:?} - We only have node names", + self + ), + }) + } else { + Ok(nodes + .iter() + .filter_map(|name| { + if self.is_accepted(name, None) { + Some(name.clone()) + } else { + None } - _ => { - return Some(Err(ColmenaError::Unknown { - message: format!("Not enough information to run rule {:?} - We only have node names", rule), - })); - } - } - } - None - }).collect() - } -} - -impl Rule { - /// Returns whether the rule matches against the NodeConfig (i.e., `config.deployment`). - pub fn matches_node_config(&self) -> bool { - match self { - Self::MatchTag(_) => true, - Self::MatchName(_) => false, + }) + .collect()) } } } @@ -177,13 +305,13 @@ mod tests { #[test] fn test_empty_filter() { let filter = NodeFilter::new("").unwrap(); - assert_eq!(0, filter.rules.len()); + assert_eq!(NodeFilter::Empty, filter); let filter = NodeFilter::new("\t").unwrap(); - assert_eq!(0, filter.rules.len()); + assert_eq!(NodeFilter::Empty, filter); let filter = NodeFilter::new(" ").unwrap(); - assert_eq!(0, filter.rules.len()); + assert_eq!(NodeFilter::Empty, filter); } #[test] @@ -197,21 +325,73 @@ mod tests { fn test_filter_rule_mixed() { let filter = NodeFilter::new("@router,gamma-*").unwrap(); assert_eq!( - vec![ - Rule::MatchTag(GlobPattern::new("router").unwrap()), - Rule::MatchName(GlobPattern::new("gamma-*").unwrap()), - ], - filter.rules, + NodeFilter::Union(vec![ + Box::new(NodeFilter::MatchTag(GlobPattern::new("router").unwrap())), + Box::new(NodeFilter::MatchName(GlobPattern::new("gamma-*").unwrap())), + ]), + filter, ); let filter = NodeFilter::new("a, \t@b , c-*").unwrap(); assert_eq!( - vec![ - Rule::MatchName(GlobPattern::new("a").unwrap()), - Rule::MatchTag(GlobPattern::new("b").unwrap()), - Rule::MatchName(GlobPattern::new("c-*").unwrap()), - ], - filter.rules, + NodeFilter::Union(vec![ + Box::new(NodeFilter::MatchName(GlobPattern::new("a").unwrap())), + Box::new(NodeFilter::MatchTag(GlobPattern::new("b").unwrap())), + Box::new(NodeFilter::MatchName(GlobPattern::new("c-*").unwrap())), + ]), + filter, + ); + + let filter = NodeFilter::new("a & \t@b , c-*").unwrap(); + assert_eq!( + NodeFilter::Inter(vec![ + Box::new(NodeFilter::MatchName(GlobPattern::new("a").unwrap())), + Box::new(NodeFilter::Union(vec![ + Box::new(NodeFilter::MatchTag(GlobPattern::new("b").unwrap())), + Box::new(NodeFilter::MatchName(GlobPattern::new("c-*").unwrap())), + ])), + ]), + filter, + ); + + let filter = NodeFilter::new("( a & \t@b ) , c-*").unwrap(); + assert_eq!( + NodeFilter::Union(vec![ + Box::new(NodeFilter::Inter(vec![ + Box::new(NodeFilter::MatchName(GlobPattern::new("a").unwrap())), + Box::new(NodeFilter::MatchTag(GlobPattern::new("b").unwrap())), + ])), + Box::new(NodeFilter::MatchName(GlobPattern::new("c-*").unwrap())), + ]), + filter, + ); + + let filter = NodeFilter::new("( a & \t@b ) , ! c-*").unwrap(); + assert_eq!( + NodeFilter::Union(vec![ + Box::new(NodeFilter::Inter(vec![ + Box::new(NodeFilter::MatchName(GlobPattern::new("a").unwrap())), + Box::new(NodeFilter::MatchTag(GlobPattern::new("b").unwrap())), + ])), + Box::new(NodeFilter::Not(Box::new(NodeFilter::MatchName( + GlobPattern::new("c-*").unwrap() + )))), + ]), + filter, + ); + + let filter = NodeFilter::new("( a & \t@b ) , !!! c-*").unwrap(); + assert_eq!( + NodeFilter::Union(vec![ + Box::new(NodeFilter::Inter(vec![ + Box::new(NodeFilter::MatchName(GlobPattern::new("a").unwrap())), + Box::new(NodeFilter::MatchTag(GlobPattern::new("b").unwrap())), + ])), + Box::new(NodeFilter::Not(Box::new(NodeFilter::MatchName( + GlobPattern::new("c-*").unwrap() + )))), + ]), + filter, ); } @@ -250,6 +430,7 @@ mod tests { privilege_escalation_command: vec![], extra_ssh_options: vec![], keys: HashMap::new(), + system_type: None, }; let mut nodes = HashMap::new(); @@ -315,5 +496,26 @@ mod tests { .unwrap() .filter_node_configs(nodes.iter()), ); + + assert_eq!( + &HashSet::from_iter([]), + &NodeFilter::new("@router&@controller") + .unwrap() + .filter_node_configs(nodes.iter()), + ); + + assert_eq!( + &HashSet::from_iter([node!("beta")]), + &NodeFilter::new("@router&@infra-*") + .unwrap() + .filter_node_configs(nodes.iter()), + ); + + assert_eq!( + &HashSet::from_iter([node!("alpha")]), + &NodeFilter::new("!@router&@infra-*") + .unwrap() + .filter_node_configs(nodes.iter()), + ); } }