commit 008eddb4ce7410f22324c90927ed7c209361f17e Author: Armaël Guéneau Date: Fri Oct 25 21:46:41 2024 +0200 Initial prototype diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2a3069 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +*~ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..72c8b8b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,225 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + +[[package]] +name = "check_diplonat_conflicts" +version = "0.1.0" +dependencies = [ + "anyhow", + "hcl-rs", + "serde", + "serde-lexpr", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hcl-edit" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a966f321ccf77cf6df2e600a3ebc8adb0b9d10acf3ed563fcf27efc69a7204" +dependencies = [ + "fnv", + "hcl-primitives", + "vecmap-rs", + "winnow", +] + +[[package]] +name = "hcl-primitives" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48fcfd8788ffb4673ac4d3c914dfae15ceedc71d6c6975ab167f9da88a1f142" +dependencies = [ + "itoa", + "kstring", + "ryu", + "serde", + "unicode-ident", +] + +[[package]] +name = "hcl-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048bb0eadcf99b53625333e7b40591309d13c35074961307c80cbf8d4729c76d" +dependencies = [ + "hcl-edit", + "hcl-primitives", + "indexmap", + "itoa", + "serde", + "vecmap-rs", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "serde", + "static_assertions", +] + +[[package]] +name = "lexpr" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a84de6a9df442363b08f5dbf0cd5b92edc70097b89c4ce4bfea4679fe48bc67" +dependencies = [ + "itoa", + "lexpr-macros", + "ryu", +] + +[[package]] +name = "lexpr-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36b5cb8bb985c81a8ac1a0f8b5c4865214f574ddd64397ef7a99c236e21f35bb" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-lexpr" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4cda13396159f59e7946118cdac0beadeecfb7cf76b197f4147e546f4ead6f" +dependencies = [ + "lexpr", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "vecmap-rs" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78fc839a22ab6c4e2f48cf5b935064188148258d467f49323134d503dd08294" +dependencies = [ + "serde", +] + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..89f2a25 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "check_diplonat_conflicts" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.91" +hcl-rs = "0.18.2" +serde = "1.0.213" +serde-lexpr = "0.1.3" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f9163a6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,169 @@ +use serde::Deserialize; +use std::{env,fs::File}; +use std::io::BufReader; +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use anyhow::{anyhow, Result}; + +mod parsing; +use parsing::*; + +// TODO: add support for (builtin?) "external" port reservations +// -> bespin, 80+443 pour forgejo + +// #[derive(Deserialize, Debug)] +// struct Config { +// #[serde(rename = "task")] +// tasks: hcl::Map, +// } + +// #[derive(Deserialize, Debug)] +// struct Task { +// #[serde(rename = "service")] +// services: Service, +// } + +// #[derive(Deserialize, Debug)] +// struct Service { +// name: String, +// } + +// #[derive(Debug)] +// struct HclVec(Vec); + +// parses a diplonat port parameter: ({tcp,udp}_port XX YY...) +// matches `DiplonatParameter` in diplonat (`src/consul_actor.rs`) +#[derive(Deserialize, Debug)] +enum DiplonatParameter { + #[serde(rename = "tcp_port")] + TcpPort(HashSet), + #[serde(rename = "udp_port")] + UdpPort(HashSet), +} + +// parses a diplonat consul tag: (diplonat ...) +// matches `DiplonatConsul` in diplonat (`src/consul_actor.rs`) +#[derive(Deserialize, Debug)] +enum DiplonatTag { + #[serde(rename = "diplonat")] + Diplonat(Vec), +} + +struct Ctx { + // Maps datacenter+port to corresponding registrations. + // If there is more than one registration, then there is a conflict. + ports: HashMap<(String, Port), Vec>, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum Port { Udp(u16), Tcp(u16) } + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct PortRegistration { + file: PathBuf, + job: String, + group: String, + task: String, + service: String, +} + +impl Ctx { + pub fn new() -> Self { + Ctx { ports: HashMap::new() } + } + + // ingest a whole .hcl file + pub fn add_file(&mut self, file: &Path) -> anyhow::Result<()> { + let cfg: Config = { + let file = File::open(file)?; + let body: hcl::Body = hcl::from_reader(BufReader::new(file))?; + Config::from_body(&body).map_err(|s| anyhow!("{}", s))? + }; + + for (jobname, job) in cfg.jobs { + for (groupname, group) in job.groups { + for (taskname, task) in group.tasks { + for service in task.services { + for tag in service.tags { + let diplo_conf: serde_lexpr::error::Result = + serde_lexpr::from_str(&tag); + if let Ok(conf) = diplo_conf { + self.add_diplonat_tag( + conf, + &job.datacenters, + PortRegistration { + file: PathBuf::from(file), + job: jobname.clone(), + group: groupname.clone(), + task: taskname.clone(), + service: service.name.clone(), + } + ); + } + } + } + } + } + } + Ok(()) + } + + // helper function: ingest a diplonat tag, of the form "(diplonat (tcp_port XX YY) (udp_port ZZ UU))" + fn add_diplonat_tag(&mut self, tag: DiplonatTag, datacenters: &[String], reg: PortRegistration) { + let DiplonatTag::Diplonat(params) = tag; + for datacenter in datacenters { + for param in ¶ms { + match param { + DiplonatParameter::TcpPort(ports) => { + for &p in ports { + self.add_port(Port::Tcp(p), datacenter.clone(), reg.clone()) + } + }, + DiplonatParameter::UdpPort(ports) => { + for &p in ports { + self.add_port(Port::Udp(p), datacenter.clone(), reg.clone()) + } + } + } + } + } + } + + // helper function: insert a port+datacenter+registration info + fn add_port(&mut self, p: Port, datacenter: String, reg: PortRegistration) { + let k = (datacenter, p); + match self.ports.get_mut(&k) { + None => { self.ports.insert(k, vec![reg]); }, + Some(ports) => ports.push(reg) + } + } +} + +fn main() -> Result { + let args: Vec<_> = env::args().collect(); + if args.len() <= 1 { + eprintln!("usage: {} ...", args[0]); + return Ok(ExitCode::FAILURE) + } + + let mut ctx = Ctx::new(); + for file in &args[1..] { + ctx.add_file(Path::new(file))? + } + + let mut have_conflicts = false; + for ((datacenter, port), regs) in ctx.ports { + if regs.len() > 1 { + have_conflicts = true; + println!("Conflict in site {}, port {:?}:", &datacenter, port); + for reg in ®s { + println!("- in {}, job \"{}\", group \"{}\", task \"{}\", service \"{}\"", + reg.file.display(), reg.job, reg.group, reg.task, reg.service) + } + println!() + } + } + + Ok(if have_conflicts { ExitCode::FAILURE } else { ExitCode::SUCCESS }) +} diff --git a/src/parsing.rs b/src/parsing.rs new file mode 100644 index 0000000..a9a29ae --- /dev/null +++ b/src/parsing.rs @@ -0,0 +1,149 @@ +use serde::Deserialize; +use hcl::{Structure, Body, Map, Expression}; + +#[derive(Deserialize, Debug)] +pub struct Config { + #[serde(rename = "job")] + pub jobs: hcl::Map, +} + +#[derive(Deserialize, Debug)] +pub struct Job { + pub datacenters: Vec, + #[serde(rename = "group")] + pub groups: hcl::Map, +} + +#[derive(Deserialize, Debug)] +pub struct Group { + #[serde(rename = "task")] + pub tasks: hcl::Map, +} + +#[derive(Deserialize, Debug)] +pub struct Task { + #[serde(rename = "service")] + pub services: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct Service { + pub name: String, + pub tags: Vec, +} + +// lots of manual boilerplate because I can't figure out how to use the +// automated deserialization for blocks without labels (e.g. "service") that may +// appear one or several times: +// https://github.com/martinohmann/hcl-rs/issues/380 + +impl Config { + pub fn from_body(b: &Body) -> Result { + Ok(Config { jobs: blocks_from_body(b, "job", Job::from_body)? }) + } +} + +impl Job { + fn from_body(b: &Body) -> Result { + let datacenters = + strings_from_expr(&attribute_from_body(b, "datacenters")?)?; + let groups = blocks_from_body(b, "group", Group::from_body)?; + Ok(Job { datacenters, groups }) + } +} + +impl Group { + fn from_body(b: &Body) -> Result { + Ok(Group { tasks: blocks_from_body(b, "task", Task::from_body)? }) + } +} + +impl Task { + fn from_body(b: &Body) -> Result { + Ok(Task { services: blocks_nolabel_from_body(b, "service", Service::from_body)? }) + } +} + +impl Service { + fn from_body(b: &Body) -> Result { + let name = string_from_expr(&attribute_from_body(b, "name")?)?; + let tags = match attribute_from_body(b, "tags") { + Err(_) => Vec::new(), + Ok(tags) => tags_from_expr(&tags)?, + }; + Ok(Service { name, tags }) + } +} + +fn blocks_from_body(b: &Body, id: &str, f: F) -> Result, String> + where F: Fn(&Body) -> Result +{ + let mut blocks = Map::new(); + for s in &b.0 { + if let Structure::Block(block) = s { + if block.identifier.as_str() == id { + if block.labels.len() != 1 { + return Err(format!("{} block: unexpected number of labels", id)) + } + blocks.insert(block.labels[0].as_str().to_string(), f(&block.body)?); + } + } + } + Ok(blocks) +} + +fn blocks_nolabel_from_body(b: &Body, id: &str, f: F) -> Result, String> + where F: Fn(&Body) -> Result +{ + let mut blocks = Vec::new(); + for s in &b.0 { + if let Structure::Block(block) = s { + if block.identifier.as_str() == id { + blocks.push(f(&block.body)?) + } + } + } + Ok(blocks) +} + +fn attribute_from_body(b: &Body, name: &str) -> Result +{ + b.0.iter().find_map(|s| { + if let Structure::Attribute(attr) = s { + if attr.key.as_str() == name { + return Some(attr.expr.clone()) + } + } + return None + }).ok_or(format!("attribute {} not found", name)) +} + +fn string_from_expr(e: &Expression) -> Result +{ + match e { + Expression::String(s) => Ok(s.to_string()), + _ => Err(format!("string expected, got {:?}", &e)), + } +} + +fn tags_from_expr(e: &Expression) -> Result, String> +{ + match e { + Expression::Array(es) => { + Ok(es.into_iter().filter_map(|e| match string_from_expr(e) { + Ok(e) => Some(e), + Err(_) => { println!("note: ignoring tag {:?}", &e); None }, + }).collect()) + }, + _ => Err(format!("array expected, got {:#?}", &e)) + } +} + +fn strings_from_expr(e: &Expression) -> Result, String> +{ + match e { + Expression::Array(es) => + es.into_iter().map(string_from_expr).collect(), + _ => Err(format!("array expected, got {:#?}", &e)) + } +}