use crate::layers::l4::loader::PreProcess;
use arc_swap::ArcSwap;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashSet;
use validator::{Validate, ValidationError, ValidationErrors, ValidationErrorsKind};
lazy_static! {
pub static ref NODES_STATE: ArcSwap<NodesConfig> = ArcSwap::default();
static ref NAME_REGEX: regex::Regex =
regex::Regex::new(r"^[a-z0-9-]+$").expect("Failed to compile NAME_REGEX");
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum IpType {
Ipv4,
Ipv6,
}
#[derive(Serialize, Deserialize, Debug, Clone, Validate, PartialEq, Eq, Hash)]
pub struct IpConfig {
#[validate(ip)]
pub address: String,
#[validate(length(min = 1, message = "must have at least one port"))]
#[serde(default)]
pub ports: Vec<u16>,
pub r#type: IpType,
}
#[derive(Serialize, Deserialize, Debug, Clone, Validate, PartialEq, Eq, Hash)]
pub struct Node {
#[validate(regex(path = *NAME_REGEX, message = "can only contain lowercase letters, numbers, and hyphens"))]
pub name: String,
#[validate(length(min = 1, message = "must have at least one IP configuration"))]
#[validate(nested)]
#[serde(default)]
pub ips: Vec<IpConfig>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ProcessedNode {
pub node_name: String,
pub address: String,
pub port: u16,
pub ip_type: IpType,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct NodesConfig {
#[serde(default)]
pub nodes: Vec<Node>,
#[serde(skip)]
pub processed: Vec<ProcessedNode>,
}
fn validate_unique_node_names(nodes: &[Node]) -> Result<(), ValidationError> {
let mut names = HashSet::new();
for node in nodes {
if !names.insert(&node.name) {
let mut err = ValidationError::new("unique_node_names");
err.message = Some(format!("Node name '{}' is not unique.", node.name).into());
return Err(err);
}
}
Ok(())
}
impl Validate for NodesConfig {
fn validate(&self) -> Result<(), ValidationErrors> {
let mut validation_errors = ValidationErrors::new();
for (i, node) in self.nodes.iter().enumerate() {
if let Err(node_errors) = node.validate() {
for (field, kind) in node_errors.errors() {
if let ValidationErrorsKind::Field(field_errors) = kind {
for error in field_errors {
let mut err = error.clone();
let old_msg = err.message.clone().unwrap_or_else(|| Cow::from("invalid"));
err.message = Some(format!("[node {i}] {field}: {old_msg}").into());
validation_errors.add("nodes", err);
}
}
}
}
}
if let Err(e) = validate_unique_node_names(&self.nodes) {
validation_errors.add("nodes", e);
}
if validation_errors.is_empty() {
Ok(())
} else {
Err(validation_errors)
}
}
}
impl PreProcess for NodesConfig {
fn pre_process(&mut self) {
let mut processed_list = Vec::new();
for node in &self.nodes {
for ip_config in &node.ips {
for &port in &ip_config.ports {
processed_list.push(ProcessedNode {
node_name: node.name.clone(),
address: ip_config.address.clone(),
port,
ip_type: ip_config.r#type.clone(),
});
}
}
}
self.processed = processed_list;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_ip_config_v4() -> IpConfig {
IpConfig {
address: "192.168.1.1".to_string(),
ports: vec![80, 443],
r#type: IpType::Ipv4,
}
}
fn valid_ip_config_v6() -> IpConfig {
IpConfig {
address: "2001:db8::1".to_string(),
ports: vec![8080],
r#type: IpType::Ipv6,
}
}
fn valid_node() -> Node {
Node {
name: "my-web-server".to_string(),
ips: vec![valid_ip_config_v4()],
}
}
#[test]
fn test_ip_config_validation() {
let mut config = valid_ip_config_v4();
assert!(config.validate().is_ok());
config.address = "not-an-ip".to_string();
assert!(config.validate().is_err());
config.address = "192.168.1.1".to_string();
config.ports = vec![];
assert!(config.validate().is_err());
}
#[test]
fn test_node_validation() {
let mut node = valid_node();
assert!(node.validate().is_ok());
node.name = "MyWebServer".to_string();
assert!(node.validate().is_err());
node.name = "my-web-server".to_string();
node.ips = vec![];
assert!(node.validate().is_err());
node.ips = vec![valid_ip_config_v4()];
node.ips[0].address = "invalid".to_string();
assert!(node.validate().is_err());
}
#[test]
fn test_nodes_config_validation() {
let mut config = NodesConfig {
nodes: vec![
valid_node(),
Node {
name: "my-db-server".to_string(),
ips: vec![valid_ip_config_v6()],
},
],
..Default::default()
};
assert!(config.validate().is_ok());
config.nodes[1].name = "my-web-server".to_string();
assert!(config.validate().is_err());
config.nodes[1].name = "my-db-server".to_string();
config.nodes[0].name = "INVALID_NAME".to_string();
assert!(config.validate().is_err());
}
#[test]
fn test_nodes_config_pre_process() {
let mut config = NodesConfig {
nodes: vec![
Node {
name: "node-a".to_string(),
ips: vec![
IpConfig {
address: "10.0.0.1".to_string(),
ports: vec![80, 81],
r#type: IpType::Ipv4,
},
IpConfig {
address: "10.0.0.2".to_string(),
ports: vec![90],
r#type: IpType::Ipv4,
},
],
},
Node {
name: "node-b".to_string(),
ips: vec![IpConfig {
address: "::1".to_string(),
ports: vec![100],
r#type: IpType::Ipv6,
}],
},
],
..Default::default()
};
assert!(config.processed.is_empty());
config.pre_process();
assert_eq!(config.processed.len(), 4);
let expected_processed = vec![
ProcessedNode {
node_name: "node-a".to_string(),
address: "10.0.0.1".to_string(),
port: 80,
ip_type: IpType::Ipv4,
},
ProcessedNode {
node_name: "node-a".to_string(),
address: "10.0.0.1".to_string(),
port: 81,
ip_type: IpType::Ipv4,
},
ProcessedNode {
node_name: "node-a".to_string(),
address: "10.0.0.2".to_string(),
port: 90,
ip_type: IpType::Ipv4,
},
ProcessedNode {
node_name: "node-b".to_string(),
address: "::1".to_string(),
port: 100,
ip_type: IpType::Ipv6,
},
];
assert_eq!(config.processed, expected_processed);
}
}