add node aliases, optimized node eval #14
6 changed files with 289 additions and 123 deletions
|
@ -81,7 +81,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
RUST_SRC_PATH = "${pkgs.rustPlatform.rustcSrc}/library";
|
RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}";
|
||||||
NIX_PATH = "nixpkgs=${pkgs.path}";
|
NIX_PATH = "nixpkgs=${pkgs.path}";
|
||||||
|
|
||||||
inputsFrom = [ defaultPackage packages.manualFast ];
|
inputsFrom = [ defaultPackage packages.manualFast ];
|
||||||
|
|
14
src/error.rs
14
src/error.rs
|
@ -6,7 +6,7 @@ use std::process::ExitStatus;
|
||||||
use snafu::{Backtrace, Snafu};
|
use snafu::{Backtrace, Snafu};
|
||||||
use validator::ValidationErrors;
|
use validator::ValidationErrors;
|
||||||
|
|
||||||
use crate::nix::{key, Profile, StorePath};
|
use crate::nix::{key, NodeName, Profile, StorePath};
|
||||||
|
|
||||||
pub type ColmenaResult<T> = Result<T, ColmenaError>;
|
pub type ColmenaResult<T> = Result<T, ColmenaError>;
|
||||||
|
|
||||||
|
@ -76,6 +76,18 @@ pub enum ColmenaError {
|
||||||
#[snafu(display("Filter rule cannot be empty"))]
|
#[snafu(display("Filter rule cannot be empty"))]
|
||||||
EmptyFilterRule,
|
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"))]
|
#[snafu(display("Deployment already executed"))]
|
||||||
DeploymentAlreadyExecuted,
|
DeploymentAlreadyExecuted,
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ mod assets;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::convert::AsRef;
|
use std::convert::AsRef;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -14,6 +14,7 @@ use tokio::sync::OnceCell;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use super::deployment::TargetNode;
|
use super::deployment::TargetNode;
|
||||||
|
use super::node_filter::{NeedsEval, PartialNodeConfig};
|
||||||
use super::{
|
use super::{
|
||||||
Flake, MetaConfig, NixExpression, NixFlags, NodeConfig, NodeFilter, NodeName,
|
Flake, MetaConfig, NixExpression, NixFlags, NodeConfig, NodeFilter, NodeName,
|
||||||
ProfileDerivation, RegistryConfig, SerializedNixExpression, StorePath,
|
ProfileDerivation, RegistryConfig, SerializedNixExpression, StorePath,
|
||||||
|
@ -260,39 +261,66 @@ impl Hive {
|
||||||
ssh_config: Option<PathBuf>,
|
ssh_config: Option<PathBuf>,
|
||||||
ssh_only: bool,
|
ssh_only: bool,
|
||||||
) -> ColmenaResult<HashMap<NodeName, TargetNode>> {
|
) -> ColmenaResult<HashMap<NodeName, TargetNode>> {
|
||||||
let mut node_configs = None;
|
|
||||||
|
|
||||||
log::info!("Enumerating systems...");
|
log::info!("Enumerating systems...");
|
||||||
let registry = self.get_registry_config().await?;
|
let registry = self.get_registry_config().await?;
|
||||||
|
|
||||||
log::info!("Enumerating nodes...");
|
log::info!("Enumerating nodes...");
|
||||||
|
|
||||||
let all_nodes = self.node_names().await?;
|
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 {
|
let selected_nodes = match filter {
|
||||||
Some(filter) => {
|
Some(filter) if filter_trivial.is_none() => {
|
||||||
if filter.has_node_config_rules() {
|
log::debug!("Retrieving deployment info for all nodes...");
|
||||||
log::debug!("Retrieving deployment info for all nodes...");
|
|
||||||
|
|
||||||
let all_node_configs = self.deployment_info().await?;
|
let needs_eval = filter.needs_eval();
|
||||||
let filtered = filter
|
|
||||||
.filter_node_configs(all_node_configs.iter())
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
node_configs = Some(all_node_configs);
|
let all_node_configs = self.deployment_info_partial(needs_eval).await?;
|
||||||
|
|
||||||
filtered
|
// Check for collisions between node names and aliases
|
||||||
} else {
|
// Returns error if:
|
||||||
filter.filter_node_names(&all_nodes)?.into_iter().collect()
|
// - 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();
|
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 {
|
let mut node_configs = if n_selected == all_nodes.len() {
|
||||||
configs.into_iter().filter(|(name, _)| selected_nodes.contains(name)).collect()
|
log::debug!("Retrieving deployment info for all nodes...");
|
||||||
|
self.deployment_info().await?
|
||||||
} else {
|
} else {
|
||||||
log::debug!("Retrieving deployment info for selected nodes...");
|
log::debug!("Retrieving deployment info for selected nodes...");
|
||||||
self.deployment_info_selected(&selected_nodes).await?
|
self.deployment_info_selected(&selected_nodes).await?
|
||||||
|
@ -396,6 +424,34 @@ impl Hive {
|
||||||
Ok(configs)
|
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(),
|
||||||
|
);
|
||||||
|
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.
|
/// Retrieve deployment info for a single node.
|
||||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||||
pub async fn deployment_info_single(
|
pub async fn deployment_info_single(
|
||||||
|
|
|
@ -93,6 +93,7 @@ with builtins; rec {
|
||||||
# Largely compatible with NixOps/Morph.
|
# Largely compatible with NixOps/Morph.
|
||||||
deploymentOptions = { name, lib, ... }: let
|
deploymentOptions = { name, lib, ... }: let
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
|
mdDoc = lib.mdDoc or lib.id;
|
||||||
|
|||||||
in {
|
in {
|
||||||
options = {
|
options = {
|
||||||
deployment = {
|
deployment = {
|
||||||
|
@ -178,6 +179,15 @@ with builtins; rec {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [];
|
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 {
|
keys = lib.mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
A set of secrets to be deployed to the node.
|
A set of secrets to be deployed to the node.
|
||||||
|
|
|
@ -75,6 +75,8 @@ pub struct NodeConfig {
|
||||||
|
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
|
|
||||||
|
aliases: Vec<NodeName>,
|
||||||
|
|
||||||
#[serde(rename = "replaceUnknownProfiles")]
|
#[serde(rename = "replaceUnknownProfiles")]
|
||||||
replace_unknown_profiles: bool,
|
replace_unknown_profiles: bool,
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use glob::Pattern as GlobPattern;
|
use glob::Pattern as GlobPattern;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{ColmenaError, ColmenaResult, NodeConfig, NodeName};
|
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>,
|
pub on: Option<NodeFilter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which fields need to be evaluated
|
||||||
|
/// in order to execute the node filter.
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct NeedsEval {
|
||||||
|
/// Need to evaluate deployment.aliases of all nodes.
|
||||||
|
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.
|
/// A filter rule.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub enum NodeFilter {
|
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,
|
/// Evaluating `config.deployment` can potentially be very expensive,
|
||||||
/// especially when its values (e.g., tags) depend on other parts of
|
/// especially when its values (e.g., tags) depend on other parts of
|
||||||
/// the configuration.
|
/// the configuration.
|
||||||
pub fn has_node_config_rules(&self) -> bool {
|
pub fn needs_eval(&self) -> NeedsEval {
|
||||||
|
// XXX: is the hashset overkill?
|
||||||
match self {
|
match self {
|
||||||
Self::MatchName(_) => false,
|
Self::MatchName(_) => NeedsEval {
|
||||||
Self::MatchTag(_) => true,
|
aliases: true,
|
||||||
Self::Union(v) => v.iter().any(|e| e.has_node_config_rules()),
|
..Default::default()
|
||||||
Self::Inter(v) => v.iter().any(|e| e.has_node_config_rules()),
|
},
|
||||||
Self::Not(e) => e.has_node_config_rules(),
|
Self::MatchTag(_) => NeedsEval {
|
||||||
Self::Empty => false,
|
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.
|
/// Decides whether a node is accepted by the filter or not.
|
||||||
/// panic if the filter depends on tags and config is None
|
/// panic if the filter depends on tags or aliases and they're None
|
||||||
fn is_accepted(&self, name: &NodeName, config: Option<&NodeConfig>) -> bool {
|
fn is_accepted(&self, name: &NodeName, config: &PartialNodeConfig) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::MatchName(pat) => pat.matches(name.as_str()),
|
Self::MatchName(pat) => {
|
||||||
|
pat.matches(name.as_str())
|
||||||
|
|| config
|
||||||
|
.aliases
|
||||||
|
.as_ref()
|
||||||
|
.expect("aliases missing")
|
||||||
|
.iter()
|
||||||
|
.any(|alias| pat.matches(&alias.0))
|
||||||
|
}
|
||||||
Self::MatchTag(pat) => config
|
Self::MatchTag(pat) => config
|
||||||
.unwrap()
|
.tags
|
||||||
.tags()
|
.as_ref()
|
||||||
|
.expect("tags missing")
|
||||||
.iter()
|
.iter()
|
||||||
.any(|tag| pat.matches(tag.as_str())),
|
.any(|tag| pat.matches(tag.as_str())),
|
||||||
Self::Union(v) => v.iter().any(|e| e.is_accepted(name, config)),
|
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.
|
/// 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
|
where
|
||||||
I: Iterator<Item = (&'a NodeName, &'a NodeConfig)>,
|
I: Iterator<Item = (&'a NodeName, &'a PartialNodeConfig)>,
|
||||||
{
|
{
|
||||||
if self == &Self::Empty {
|
if self == &Self::Empty {
|
||||||
return HashSet::new();
|
return HashSet::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes
|
nodes
|
||||||
.filter_map(|(name, node)| {
|
.filter_map(|(name, config)| {
|
||||||
if self.is_accepted(name, Some(node)) {
|
if self.is_accepted(name, config) {
|
||||||
Some(name)
|
Some(name)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -294,26 +359,34 @@ impl NodeFilter {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs the filter against a set of node names and returns the matched ones.
|
/// In case of trivial filters which dont actually use any node info
|
||||||
pub fn filter_node_names(&self, nodes: &[NodeName]) -> ColmenaResult<HashSet<NodeName>> {
|
/// Try to eval them immediately
|
||||||
if self.has_node_config_rules() {
|
pub fn try_eval_trivial(&self) -> Option<bool> {
|
||||||
Err(ColmenaError::Unknown {
|
match self {
|
||||||
message: format!(
|
Self::MatchName(_) => None,
|
||||||
"Not enough information to run rule {:?} - We only have node names",
|
Self::MatchTag(_) => None,
|
||||||
self
|
Self::Union(fs) => {
|
||||||
),
|
for f in fs {
|
||||||
})
|
match f.try_eval_trivial() {
|
||||||
} else {
|
None => return None,
|
||||||
Ok(nodes
|
Some(true) => return Some(true),
|
||||||
.iter()
|
Some(false) => continue,
|
||||||
.filter_map(|name| {
|
|
||||||
if self.is_accepted(name, None) {
|
|
||||||
Some(name.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.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 {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
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};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
macro_rules! node {
|
macro_rules! node {
|
||||||
|
@ -424,126 +527,109 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_filter_node_names() {
|
fn test_filter_nodes_names_only() {
|
||||||
let nodes = vec![node!("lax-alpha"), node!("lax-beta"), node!("sfo-gamma")];
|
let nodes = vec![
|
||||||
|
(node!("lax-alpha"), PartialNodeConfig::known_empty()),
|
||||||
|
(node!("lax-beta"), PartialNodeConfig::known_empty()),
|
||||||
|
(node!("sfo-gamma"), PartialNodeConfig::known_empty()),
|
||||||
|
];
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([node!("lax-alpha")]),
|
&HashSet::from_iter([node!("lax-alpha")]),
|
||||||
&NodeFilter::new("lax-alpha")
|
&NodeFilter::new("lax-alpha")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_node_names(&nodes)
|
.filter_nodes(nodes.iter().map(|x| (&x.0, &x.1))),
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([node!("lax-alpha"), node!("lax-beta")]),
|
&HashSet::from_iter([node!("lax-alpha"), node!("lax-beta")]),
|
||||||
&NodeFilter::new("lax-*")
|
&NodeFilter::new("lax-*")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_node_names(&nodes)
|
.filter_nodes(nodes.iter().map(|x| (&x.0, &x.1))),
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_filter_node_configs() {
|
fn test_filter_nodes() {
|
||||||
// TODO: Better way to mock
|
let nodes: HashMap<NodeName, PartialNodeConfig> = HashMap::from([
|
||||||
let template = NodeConfig {
|
(
|
||||||
tags: vec![],
|
node!("alpha"),
|
||||||
target_host: None,
|
PartialNodeConfig::known_tags(vec!["web".to_string(), "infra-lax".to_string()]),
|
||||||
target_user: None,
|
),
|
||||||
target_port: None,
|
(
|
||||||
allow_local_deployment: false,
|
node!("beta"),
|
||||||
build_on_target: false,
|
PartialNodeConfig::known_tags(vec!["router".to_string(), "infra-sfo".to_string()]),
|
||||||
replace_unknown_profiles: false,
|
),
|
||||||
privilege_escalation_command: vec![],
|
(
|
||||||
extra_ssh_options: vec![],
|
node!("gamma-a"),
|
||||||
keys: HashMap::new(),
|
PartialNodeConfig::known_tags(vec!["controller".to_string()]),
|
||||||
system_type: None,
|
),
|
||||||
};
|
(
|
||||||
|
node!("gamma-b"),
|
||||||
let mut nodes = HashMap::new();
|
PartialNodeConfig::known_tags(vec!["ewaste".to_string()]),
|
||||||
|
),
|
||||||
nodes.insert(
|
(
|
||||||
node!("alpha"),
|
node!("aliases-test"),
|
||||||
NodeConfig {
|
PartialNodeConfig::known_aliases_tags(
|
||||||
tags: vec!["web".to_string(), "infra-lax".to_string()],
|
Some(vec![node!("whatever-alias1"), node!("whatever-alias2")]),
|
||||||
..template.clone()
|
Some(vec!["testing".into()]),
|
||||||
},
|
),
|
||||||
);
|
),
|
||||||
|
]);
|
||||||
nodes.insert(
|
assert_eq!(5, nodes.len());
|
||||||
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());
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([node!("alpha")]),
|
&HashSet::from_iter([node!("alpha")]),
|
||||||
&NodeFilter::new("@web")
|
&NodeFilter::new("@web").unwrap().filter_nodes(nodes.iter()),
|
||||||
.unwrap()
|
|
||||||
.filter_node_configs(nodes.iter()),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([node!("alpha"), node!("beta")]),
|
&HashSet::from_iter([node!("alpha"), node!("beta")]),
|
||||||
&NodeFilter::new("@infra-*")
|
&NodeFilter::new("@infra-*")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_node_configs(nodes.iter()),
|
.filter_nodes(nodes.iter()),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([node!("beta"), node!("gamma-a")]),
|
&HashSet::from_iter([node!("beta"), node!("gamma-a")]),
|
||||||
&NodeFilter::new("@router,@controller")
|
&NodeFilter::new("@router,@controller")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_node_configs(nodes.iter()),
|
.filter_nodes(nodes.iter()),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([node!("beta"), node!("gamma-a"), node!("gamma-b")]),
|
&HashSet::from_iter([node!("beta"), node!("gamma-a"), node!("gamma-b")]),
|
||||||
&NodeFilter::new("@router,gamma-*")
|
&NodeFilter::new("@router,gamma-*")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_node_configs(nodes.iter()),
|
.filter_nodes(nodes.iter()),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([]),
|
&HashSet::from_iter([]),
|
||||||
&NodeFilter::new("@router&@controller")
|
&NodeFilter::new("@router&@controller")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_node_configs(nodes.iter()),
|
.filter_nodes(nodes.iter()),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([node!("beta")]),
|
&HashSet::from_iter([node!("beta")]),
|
||||||
&NodeFilter::new("@router&@infra-*")
|
&NodeFilter::new("@router&@infra-*")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_node_configs(nodes.iter()),
|
.filter_nodes(nodes.iter()),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&HashSet::from_iter([node!("alpha")]),
|
&HashSet::from_iter([node!("alpha")]),
|
||||||
&NodeFilter::new("!@router&@infra-*")
|
&NodeFilter::new("!@router&@infra-*")
|
||||||
.unwrap()
|
.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()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue
Why is
mdDoc
needed? That seems dead code to me.that part is from the #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)