Initial prototype
This commit is contained in:
commit
7e9cfea9e0
5 changed files with 537 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
*~
|
225
Cargo.lock
generated
Normal file
225
Cargo.lock
generated
Normal file
|
@ -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",
|
||||
]
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
|
@ -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"
|
149
src/main.rs
Normal file
149
src/main.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
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
|
||||
|
||||
// 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<u16>),
|
||||
#[serde(rename = "udp_port")]
|
||||
UdpPort(HashSet<u16>),
|
||||
}
|
||||
|
||||
// parses a diplonat consul tag: (diplonat <param>...)
|
||||
// matches `DiplonatConsul` in diplonat (`src/consul_actor.rs`)
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum DiplonatTag {
|
||||
#[serde(rename = "diplonat")]
|
||||
Diplonat(Vec<DiplonatParameter>),
|
||||
}
|
||||
|
||||
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<PortRegistration>>,
|
||||
}
|
||||
|
||||
#[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<DiplonatTag> =
|
||||
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<ExitCode> {
|
||||
let args: Vec<_> = env::args().collect();
|
||||
if args.len() <= 1 {
|
||||
eprintln!("usage: {} <file.hcl>...", 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 })
|
||||
}
|
149
src/parsing.rs
Normal file
149
src/parsing.rs
Normal file
|
@ -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<String, Job>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Job {
|
||||
pub datacenters: Vec<String>,
|
||||
#[serde(rename = "group")]
|
||||
pub groups: hcl::Map<String, Group>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Group {
|
||||
#[serde(rename = "task")]
|
||||
pub tasks: hcl::Map<String, Task>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Task {
|
||||
#[serde(rename = "service")]
|
||||
pub services: Vec<Service>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Service {
|
||||
pub name: String,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
// 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<Self, String> {
|
||||
Ok(Config { jobs: blocks_from_body(b, "job", Job::from_body)? })
|
||||
}
|
||||
}
|
||||
|
||||
impl Job {
|
||||
fn from_body(b: &Body) -> Result<Self, String> {
|
||||
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<Self, String> {
|
||||
Ok(Group { tasks: blocks_from_body(b, "task", Task::from_body)? })
|
||||
}
|
||||
}
|
||||
|
||||
impl Task {
|
||||
fn from_body(b: &Body) -> Result<Self, String> {
|
||||
Ok(Task { services: blocks_nolabel_from_body(b, "service", Service::from_body)? })
|
||||
}
|
||||
}
|
||||
|
||||
impl Service {
|
||||
fn from_body(b: &Body) -> Result<Self, String> {
|
||||
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<T, F>(b: &Body, id: &str, f: F) -> Result<Map<String, T>, String>
|
||||
where F: Fn(&Body) -> Result<T, String>
|
||||
{
|
||||
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<T, F>(b: &Body, id: &str, f: F) -> Result<Vec<T>, String>
|
||||
where F: Fn(&Body) -> Result<T, String>
|
||||
{
|
||||
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<Expression, String>
|
||||
{
|
||||
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<String, String>
|
||||
{
|
||||
match e {
|
||||
Expression::String(s) => Ok(s.to_string()),
|
||||
_ => Err(format!("string expected, got {:?}", &e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn tags_from_expr(e: &Expression) -> Result<Vec<String>, 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<Vec<String>, String>
|
||||
{
|
||||
match e {
|
||||
Expression::Array(es) =>
|
||||
es.into_iter().map(string_from_expr).collect(),
|
||||
_ => Err(format!("array expected, got {:#?}", &e))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue