add node aliases, optimized node eval #14
|
@ -81,7 +81,7 @@
|
|||
};
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
RUST_SRC_PATH = "${pkgs.rustPlatform.rustcSrc}/library";
|
||||
RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}";
|
||||
NIX_PATH = "nixpkgs=${pkgs.path}";
|
||||
|
||||
inputsFrom = [ defaultPackage packages.manualFast ];
|
||||
|
|
14
src/error.rs
|
@ -6,7 +6,7 @@ use std::process::ExitStatus;
|
|||
use snafu::{Backtrace, Snafu};
|
||||
use validator::ValidationErrors;
|
||||
|
||||
use crate::nix::{key, Profile, StorePath};
|
||||
use crate::nix::{key, NodeName, Profile, StorePath};
|
||||
|
||||
pub type ColmenaResult<T> = Result<T, ColmenaError>;
|
||||
|
||||
|
@ -76,6 +76,18 @@ pub enum ColmenaError {
|
|||
#[snafu(display("Filter rule cannot be empty"))]
|
||||
EmptyFilterRule,
|
||||
|
||||
#[snafu(display(
|
||||
"Alias \"{}\" is already taken by {} \"{}\"",
|
||||
what.as_str(),
|
||||
if *is_node_name { "node name" } else { "alias" },
|
||||
with.as_str(),
|
||||
))]
|
||||
DuplicateAlias {
|
||||
what: NodeName,
|
||||
is_node_name: bool,
|
||||
with: NodeName,
|
||||
},
|
||||
|
||||
#[snafu(display("Deployment already executed"))]
|
||||
DeploymentAlreadyExecuted,
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ mod assets;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::AsRef;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
@ -14,6 +14,7 @@ use tokio::sync::OnceCell;
|
|||
use validator::Validate;
|
||||
|
||||
use super::deployment::TargetNode;
|
||||
use super::node_filter::{NeedsEval, PartialNodeConfig};
|
||||
use super::{
|
||||
Flake, MetaConfig, NixExpression, NixFlags, NodeConfig, NodeFilter, NodeName,
|
||||
ProfileDerivation, RegistryConfig, SerializedNixExpression, StorePath,
|
||||
|
@ -260,39 +261,66 @@ impl Hive {
|
|||
ssh_config: Option<PathBuf>,
|
||||
ssh_only: bool,
|
||||
) -> ColmenaResult<HashMap<NodeName, TargetNode>> {
|
||||
let mut node_configs = None;
|
||||
|
||||
log::info!("Enumerating systems...");
|
||||
let registry = self.get_registry_config().await?;
|
||||
|
||||
log::info!("Enumerating nodes...");
|
||||
|
||||
let all_nodes = self.node_names().await?;
|
||||
|
||||
// try to quickly evaluate the filter without any data to see if it's trivial to evaluate
|
||||
let filter_trivial = filter.as_ref().and_then(|filter| filter.try_eval_trivial());
|
||||
|
||||
let selected_nodes = match filter {
|
||||
Some(filter) => {
|
||||
if filter.has_node_config_rules() {
|
||||
log::debug!("Retrieving deployment info for all nodes...");
|
||||
Some(filter) if filter_trivial.is_none() => {
|
||||
log::debug!("Retrieving deployment info for all nodes...");
|
||||
|
||||
let all_node_configs = self.deployment_info().await?;
|
||||
let filtered = filter
|
||||
.filter_node_configs(all_node_configs.iter())
|
||||
.into_iter()
|
||||
.collect();
|
||||
let needs_eval = filter.needs_eval();
|
||||
|
||||
node_configs = Some(all_node_configs);
|
||||
let all_node_configs = self.deployment_info_partial(needs_eval).await?;
|
||||
|
||||
filtered
|
||||
} else {
|
||||
filter.filter_node_names(&all_nodes)?.into_iter().collect()
|
||||
// Check for collisions between node names and aliases
|
||||
// Returns error if:
|
||||
// - A node has an alias matching another node's name
|
||||
// - A node has an alias matching its own name
|
||||
// - A node has an alias already used by another node
|
||||
if needs_eval.aliases {
|
||||
let mut taken_aliases = HashSet::new();
|
||||
for (name, config) in all_node_configs.iter() {
|
||||
for alias in config.aliases.as_ref().unwrap().iter() {
|
||||
let overlaps_this = alias == name;
|
||||
let overlaps_names = all_node_configs.contains_key(alias);
|
||||
let overlaps_aliases = !taken_aliases.insert(alias.clone());
|
||||
if overlaps_this || overlaps_names || overlaps_aliases {
|
||||
return Err(ColmenaError::DuplicateAlias {
|
||||
what: alias.clone(),
|
||||
is_node_name: overlaps_this || overlaps_names,
|
||||
with: name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filtered = filter
|
||||
.filter_nodes(all_node_configs.iter())
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
filtered
|
||||
}
|
||||
None => all_nodes.clone(),
|
||||
_ => match filter_trivial {
|
||||
// Filter is known to always evaluate to no nodes
|
||||
Some(false) => vec![],
|
||||
_ => all_nodes.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
let n_selected = selected_nodes.len();
|
||||
log::debug!("Filtered {n_selected} node names for deployment");
|
||||
|
||||
let mut node_configs = if let Some(configs) = node_configs {
|
||||
configs.into_iter().filter(|(name, _)| selected_nodes.contains(name)).collect()
|
||||
let mut node_configs = if n_selected == all_nodes.len() {
|
||||
log::debug!("Retrieving deployment info for all nodes...");
|
||||
self.deployment_info().await?
|
||||
} else {
|
||||
log::debug!("Retrieving deployment info for selected nodes...");
|
||||
self.deployment_info_selected(&selected_nodes).await?
|
||||
|
@ -396,6 +424,34 @@ impl Hive {
|
|||
Ok(configs)
|
||||
}
|
||||
|
||||
pub async fn deployment_info_partial(
|
||||
&self,
|
||||
needs_eval: NeedsEval,
|
||||
) -> ColmenaResult<HashMap<NodeName, PartialNodeConfig>> {
|
||||
if !needs_eval.any() {
|
||||
// Need just the un-aliased names
|
||||
return Ok(self
|
||||
.node_names()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|name| (name, PartialNodeConfig::default()))
|
||||
.collect());
|
||||
}
|
||||
|
||||
let expr = format!(
|
||||
"(mapAttrs (name: attrs: {{ inherit (attrs) {} {}; }}) hive.deploymentConfig)",
|
||||
needs_eval.aliases.then_some("aliases").unwrap_or_default(),
|
||||
needs_eval.tags.then_some("tags").unwrap_or_default(),
|
||||
lbailly marked this conversation as resolved
Outdated
|
||||
);
|
||||
let configs: HashMap<NodeName, PartialNodeConfig> = self
|
||||
.nix_instantiate(&expr)
|
||||
.eval_with_builders()
|
||||
.await?
|
||||
.capture_json()
|
||||
.await?;
|
||||
Ok(configs)
|
||||
}
|
||||
|
||||
/// Retrieve deployment info for a single node.
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
pub async fn deployment_info_single(
|
||||
|
|
|
@ -93,6 +93,7 @@ with builtins; rec {
|
|||
# Largely compatible with NixOps/Morph.
|
||||
deploymentOptions = { name, lib, ... }: let
|
||||
inherit (lib) types;
|
||||
mdDoc = lib.mdDoc or lib.id;
|
||||
rlahfa
commented
Why is Why is `mdDoc` needed? That seems dead code to me.
griffi-gh
commented
that part is from the #13 (idk what the mdDoc is for, but the flake was failing to evaluate because of a missing variable that part is from the https://git.dgnum.eu/DGNum/colmena/pulls/13
since i based the pr on it
(idk what the mdDoc is for, but the flake was failing to evaluate because of a missing variable
so i copied the same let binding that was used in the other 2 sections)
|
||||
in {
|
||||
options = {
|
||||
deployment = {
|
||||
|
@ -178,6 +179,15 @@ with builtins; rec {
|
|||
type = types.listOf types.str;
|
||||
default = [];
|
||||
};
|
||||
aliases = lib.mkOption {
|
||||
description = ''
|
||||
A list of aliases for the node.
|
||||
|
||||
Can be used to select a node with another name.
|
||||
'';
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
};
|
||||
keys = lib.mkOption {
|
||||
description = ''
|
||||
A set of secrets to be deployed to the node.
|
||||
|
|
|
@ -75,6 +75,8 @@ pub struct NodeConfig {
|
|||
|
||||
tags: Vec<String>,
|
||||
|
||||
aliases: Vec<NodeName>,
|
||||
|
||||
#[serde(rename = "replaceUnknownProfiles")]
|
||||
replace_unknown_profiles: bool,
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::str::FromStr;
|
|||
|
||||
use clap::Args;
|
||||
use glob::Pattern as GlobPattern;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{ColmenaError, ColmenaResult, NodeConfig, NodeName};
|
||||
|
||||
|
@ -28,6 +29,53 @@ The list is comma-separated and globs are supported. To match tags, prepend the
|
|||
pub on: Option<NodeFilter>,
|
||||
}
|
||||
|
||||
/// Which fields need to be evaluated
|
||||
/// in order to execute the node filter.
|
||||
griffi-gh marked this conversation as resolved
Outdated
rlahfa
commented
What is What is `NeedsEval`, can we have a comment on what is this structure supposed to carry?
griffi-gh
commented
which parts of the deployment metadata need to be evaluated to run the filter. which parts of the deployment metadata need to be evaluated to run the filter.
(Should probably add a doc comment)
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct NeedsEval {
|
||||
/// Need to evaluate deployment.aliases of all nodes.
|
||||
griffi-gh marked this conversation as resolved
Outdated
lbailly
commented
You can remove You can remove
|
||||
pub aliases: bool,
|
||||
/// Need to evaluate deployment.tags of all nodes.
|
||||
pub tags: bool,
|
||||
}
|
||||
|
||||
impl NeedsEval {
|
||||
pub fn any(&self) -> bool {
|
||||
self.aliases || self.tags
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for NeedsEval {
|
||||
type Output = Self;
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
aliases: self.aliases || rhs.aliases,
|
||||
tags: self.tags || rhs.tags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOrAssign for NeedsEval {
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
*self = *self | rhs;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct PartialNodeConfig {
|
||||
pub aliases: Option<Vec<NodeName>>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl From<NodeConfig> for PartialNodeConfig {
|
||||
fn from(node_config: NodeConfig) -> Self {
|
||||
Self {
|
||||
aliases: Some(node_config.aliases),
|
||||
tags: Some(node_config.tags),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A filter rule.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum NodeFilter {
|
||||
|
@ -240,30 +288,47 @@ impl NodeFilter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns whether the filter has any rule matching NodeConfig information.
|
||||
/// Returns which NodeConfig information is needed to evaluate the filter.
|
||||
///
|
||||
/// Evaluating `config.deployment` can potentially be very expensive,
|
||||
/// especially when its values (e.g., tags) depend on other parts of
|
||||
/// the configuration.
|
||||
pub fn has_node_config_rules(&self) -> bool {
|
||||
pub fn needs_eval(&self) -> NeedsEval {
|
||||
// XXX: is the hashset overkill?
|
||||
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,
|
||||
Self::MatchName(_) => NeedsEval {
|
||||
aliases: true,
|
||||
..Default::default()
|
||||
},
|
||||
Self::MatchTag(_) => NeedsEval {
|
||||
tags: true,
|
||||
..Default::default()
|
||||
},
|
||||
Self::Union(v) | Self::Inter(v) => v
|
||||
.iter()
|
||||
.fold(NeedsEval::default(), |acc, e| acc | e.needs_eval()),
|
||||
Self::Not(e) => e.needs_eval(),
|
||||
Self::Empty => NeedsEval::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decides whether a node is accepted by the filter or not.
|
||||
/// panic if the filter depends on tags and config is None
|
||||
fn is_accepted(&self, name: &NodeName, config: Option<&NodeConfig>) -> bool {
|
||||
/// panic if the filter depends on tags or aliases and they're None
|
||||
fn is_accepted(&self, name: &NodeName, config: &PartialNodeConfig) -> bool {
|
||||
match self {
|
||||
Self::MatchName(pat) => pat.matches(name.as_str()),
|
||||
Self::MatchName(pat) => {
|
||||
pat.matches(name.as_str())
|
||||
rlahfa marked this conversation as resolved
Outdated
rlahfa
commented
`expect` means we are crashing here, can we have better error reporting?
|
||||
|| config
|
||||
.aliases
|
||||
.as_ref()
|
||||
.expect("aliases missing")
|
||||
.iter()
|
||||
.any(|alias| pat.matches(&alias.0))
|
||||
}
|
||||
rlahfa marked this conversation as resolved
Outdated
rlahfa
commented
`expect` means we are crashing here, can we have better error reporting?
griffi-gh
commented
this error should never happen as tags are guaranteed to be gathered by the previous step (even the tags are empty, the field should never be None) Panic if tags/deployment info is missing was also the original behavior before the rewrite... same sith the aliases one this error should never happen as tags are guaranteed to be gathered by the previous step (even the tags are empty, the field should never be None)
Panic if tags/deployment info is missing was also the original behavior before the rewrite...
same sith the aliases one
rlahfa
commented
makes sense makes sense
|
||||
Self::MatchTag(pat) => config
|
||||
.unwrap()
|
||||
.tags()
|
||||
.tags
|
||||
.as_ref()
|
||||
.expect("tags missing")
|
||||
.iter()
|
||||
.any(|tag| pat.matches(tag.as_str())),
|
||||
Self::Union(v) => v.iter().any(|e| e.is_accepted(name, config)),
|
||||
|
@ -274,17 +339,17 @@ impl NodeFilter {
|
|||
}
|
||||
|
||||
/// Runs the filter against a set of NodeConfigs and returns the matched ones.
|
||||
pub fn filter_node_configs<'a, I>(&self, nodes: I) -> HashSet<NodeName>
|
||||
pub fn filter_nodes<'a, I>(&self, nodes: I) -> HashSet<NodeName>
|
||||
where
|
||||
I: Iterator<Item = (&'a NodeName, &'a NodeConfig)>,
|
||||
I: Iterator<Item = (&'a NodeName, &'a PartialNodeConfig)>,
|
||||
{
|
||||
if self == &Self::Empty {
|
||||
return HashSet::new();
|
||||
}
|
||||
|
||||
nodes
|
||||
.filter_map(|(name, node)| {
|
||||
if self.is_accepted(name, Some(node)) {
|
||||
.filter_map(|(name, config)| {
|
||||
if self.is_accepted(name, config) {
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
|
@ -294,26 +359,34 @@ impl NodeFilter {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Runs the filter against a set of node names and returns the matched ones.
|
||||
pub fn filter_node_names(&self, nodes: &[NodeName]) -> ColmenaResult<HashSet<NodeName>> {
|
||||
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
|
||||
/// In case of trivial filters which dont actually use any node info
|
||||
/// Try to eval them immediately
|
||||
pub fn try_eval_trivial(&self) -> Option<bool> {
|
||||
match self {
|
||||
Self::MatchName(_) => None,
|
||||
lbailly marked this conversation as resolved
Outdated
lbailly
commented
Currently, the only trivial filter is Currently, the only trivial filter is `Empty` as it's not possible to build trivial `Union` or `Inter` (trivial = size < 2), nor to have an `Empty` in a leaf of a more complex filter.
lbailly
commented
(In syntactic analysis, (In syntactic analysis, `!a&a` is trivial, but require a semantic analysis, which I don't think we want)
griffi-gh
commented
Yea, I agree that it's kinda over-engineered... Should i change this? Yea, I agree that it's kinda over-engineered...
(but it does handle the Empty case so i guess its fine?)
Should i change this?
lbailly
commented
I don't think it's necessary to change this, this over-engineering won't weight much and will make future development cleaner (if the invariant was invalidated for example). I don't think it's necessary to change this, this over-engineering won't weight much and will make future development cleaner (if the invariant was invalidated for example).
|
||||
Self::MatchTag(_) => None,
|
||||
Self::Union(fs) => {
|
||||
for f in fs {
|
||||
match f.try_eval_trivial() {
|
||||
None => return None,
|
||||
Some(true) => return Some(true),
|
||||
Some(false) => continue,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
Some(false)
|
||||
}
|
||||
Self::Inter(fs) => {
|
||||
for f in fs {
|
||||
match f.try_eval_trivial() {
|
||||
None => return None,
|
||||
Some(true) => continue,
|
||||
Some(false) => return Some(false),
|
||||
}
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
Self::Not(f) => f.try_eval_trivial().map(|b| !b),
|
||||
Self::Empty => Some(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -322,6 +395,36 @@ impl NodeFilter {
|
|||
mod tests {
|
||||
use super::*;
|
||||
griffi-gh marked this conversation as resolved
Outdated
lbailly
commented
they look good to me, no ? they look good to me, no ?
griffi-gh
commented
yeah i forgot to remove it yeah i forgot to remove it
i already fixed them in ba6a9dc798d77b3e32dfb1302543827cbaaf3a34
|
||||
|
||||
impl PartialNodeConfig {
|
||||
fn known_empty() -> Self {
|
||||
Self {
|
||||
aliases: Some(vec![]),
|
||||
tags: Some(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn known_aliases_tags(
|
||||
aliases: Option<Vec<NodeName>>,
|
||||
tags: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
Self { aliases, tags }
|
||||
}
|
||||
|
||||
fn known_tags(tags: Vec<String>) -> Self {
|
||||
Self {
|
||||
aliases: Some(vec![]),
|
||||
tags: Some(tags),
|
||||
}
|
||||
}
|
||||
|
||||
fn known_aliases(aliases: Vec<NodeName>) -> Self {
|
||||
Self {
|
||||
aliases: Some(aliases),
|
||||
tags: Some(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
macro_rules! node {
|
||||
|
@ -424,126 +527,109 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_node_names() {
|
||||
let nodes = vec![node!("lax-alpha"), node!("lax-beta"), node!("sfo-gamma")];
|
||||
fn test_filter_nodes_names_only() {
|
||||
let nodes = vec![
|
||||
(node!("lax-alpha"), PartialNodeConfig::known_empty()),
|
||||
(node!("lax-beta"), PartialNodeConfig::known_empty()),
|
||||
(node!("sfo-gamma"), PartialNodeConfig::known_empty()),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("lax-alpha")]),
|
||||
&NodeFilter::new("lax-alpha")
|
||||
.unwrap()
|
||||
.filter_node_names(&nodes)
|
||||
.unwrap(),
|
||||
.filter_nodes(nodes.iter().map(|x| (&x.0, &x.1))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("lax-alpha"), node!("lax-beta")]),
|
||||
&NodeFilter::new("lax-*")
|
||||
.unwrap()
|
||||
.filter_node_names(&nodes)
|
||||
.unwrap(),
|
||||
.filter_nodes(nodes.iter().map(|x| (&x.0, &x.1))),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_node_configs() {
|
||||
// TODO: Better way to mock
|
||||
let template = NodeConfig {
|
||||
tags: vec![],
|
||||
target_host: None,
|
||||
target_user: None,
|
||||
target_port: None,
|
||||
allow_local_deployment: false,
|
||||
build_on_target: false,
|
||||
replace_unknown_profiles: false,
|
||||
privilege_escalation_command: vec![],
|
||||
extra_ssh_options: vec![],
|
||||
keys: HashMap::new(),
|
||||
system_type: None,
|
||||
};
|
||||
|
||||
let mut nodes = HashMap::new();
|
||||
|
||||
nodes.insert(
|
||||
node!("alpha"),
|
||||
NodeConfig {
|
||||
tags: vec!["web".to_string(), "infra-lax".to_string()],
|
||||
..template.clone()
|
||||
},
|
||||
);
|
||||
|
||||
nodes.insert(
|
||||
node!("beta"),
|
||||
NodeConfig {
|
||||
tags: vec!["router".to_string(), "infra-sfo".to_string()],
|
||||
..template.clone()
|
||||
},
|
||||
);
|
||||
|
||||
nodes.insert(
|
||||
node!("gamma-a"),
|
||||
NodeConfig {
|
||||
tags: vec!["controller".to_string()],
|
||||
..template.clone()
|
||||
},
|
||||
);
|
||||
|
||||
nodes.insert(
|
||||
node!("gamma-b"),
|
||||
NodeConfig {
|
||||
tags: vec!["ewaste".to_string()],
|
||||
..template
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(4, nodes.len());
|
||||
fn test_filter_nodes() {
|
||||
let nodes: HashMap<NodeName, PartialNodeConfig> = HashMap::from([
|
||||
(
|
||||
node!("alpha"),
|
||||
PartialNodeConfig::known_tags(vec!["web".to_string(), "infra-lax".to_string()]),
|
||||
),
|
||||
(
|
||||
node!("beta"),
|
||||
PartialNodeConfig::known_tags(vec!["router".to_string(), "infra-sfo".to_string()]),
|
||||
),
|
||||
(
|
||||
node!("gamma-a"),
|
||||
PartialNodeConfig::known_tags(vec!["controller".to_string()]),
|
||||
),
|
||||
(
|
||||
node!("gamma-b"),
|
||||
PartialNodeConfig::known_tags(vec!["ewaste".to_string()]),
|
||||
),
|
||||
(
|
||||
node!("aliases-test"),
|
||||
PartialNodeConfig::known_aliases_tags(
|
||||
Some(vec![node!("whatever-alias1"), node!("whatever-alias2")]),
|
||||
Some(vec!["testing".into()]),
|
||||
),
|
||||
),
|
||||
]);
|
||||
assert_eq!(5, nodes.len());
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("alpha")]),
|
||||
&NodeFilter::new("@web")
|
||||
.unwrap()
|
||||
.filter_node_configs(nodes.iter()),
|
||||
&NodeFilter::new("@web").unwrap().filter_nodes(nodes.iter()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("alpha"), node!("beta")]),
|
||||
&NodeFilter::new("@infra-*")
|
||||
.unwrap()
|
||||
.filter_node_configs(nodes.iter()),
|
||||
.filter_nodes(nodes.iter()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("beta"), node!("gamma-a")]),
|
||||
&NodeFilter::new("@router,@controller")
|
||||
.unwrap()
|
||||
.filter_node_configs(nodes.iter()),
|
||||
.filter_nodes(nodes.iter()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("beta"), node!("gamma-a"), node!("gamma-b")]),
|
||||
&NodeFilter::new("@router,gamma-*")
|
||||
.unwrap()
|
||||
.filter_node_configs(nodes.iter()),
|
||||
.filter_nodes(nodes.iter()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([]),
|
||||
&NodeFilter::new("@router&@controller")
|
||||
.unwrap()
|
||||
.filter_node_configs(nodes.iter()),
|
||||
.filter_nodes(nodes.iter()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("beta")]),
|
||||
&NodeFilter::new("@router&@infra-*")
|
||||
.unwrap()
|
||||
.filter_node_configs(nodes.iter()),
|
||||
.filter_nodes(nodes.iter()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("alpha")]),
|
||||
&NodeFilter::new("!@router&@infra-*")
|
||||
.unwrap()
|
||||
.filter_node_configs(nodes.iter()),
|
||||
.filter_nodes(nodes.iter()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&HashSet::from_iter([node!("aliases-test")]),
|
||||
&NodeFilter::new("whatever-alias1")
|
||||
.unwrap()
|
||||
.filter_nodes(nodes.iter()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
no