Compare commits

...

No commits in common. "main" and "main@origin" have entirely different histories.

2 changed files with 153 additions and 88 deletions

View file

@ -1,14 +1,37 @@
use serde::Deserialize; use serde::Deserialize;
use std::{env,fmt,fs::File}; use std::{env,fs::File};
use std::io::BufReader; use std::io::BufReader;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::ExitCode; use std::process::ExitCode;
use anyhow::Result; use anyhow::{anyhow, Result};
mod parsing; mod parsing;
use 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<String, Task>,
// }
// #[derive(Deserialize, Debug)]
// struct Task {
// #[serde(rename = "service")]
// services: Service,
// }
// #[derive(Deserialize, Debug)]
// struct Service {
// name: String,
// }
// #[derive(Debug)]
// struct HclVec<T>(Vec<T>);
// parses a diplonat port parameter: ({tcp,udp}_port XX YY...) // parses a diplonat port parameter: ({tcp,udp}_port XX YY...)
// matches `DiplonatParameter` in diplonat (`src/consul_actor.rs`) // matches `DiplonatParameter` in diplonat (`src/consul_actor.rs`)
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -37,31 +60,12 @@ struct Ctx {
enum Port { Udp(u16), Tcp(u16) } enum Port { Udp(u16), Tcp(u16) }
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum PortRegistration { struct PortRegistration {
Config {
file: PathBuf, file: PathBuf,
job: String, job: String,
group: String, group: String,
task: String, task: String,
service: Option<String> service: String,
},
Builtin(String),
}
impl fmt::Display for PortRegistration {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PortRegistration::Config { file, job, group, task, service } =>
write!(f, "in {}, job \"{}\", group \"{}\", task \"{}\", service \"{}\"",
file.display(), job, group, task,
match &service {
None => "<noname>",
Some(s) => s
}),
PortRegistration::Builtin(reason) =>
write!(f, "{}", reason),
}
}
} }
impl Ctx { impl Ctx {
@ -73,8 +77,8 @@ impl Ctx {
pub fn add_file(&mut self, file: &Path) -> anyhow::Result<()> { pub fn add_file(&mut self, file: &Path) -> anyhow::Result<()> {
let cfg: Config = { let cfg: Config = {
let file = File::open(file)?; let file = File::open(file)?;
let config: Config = hcl::from_reader(BufReader::new(file))?; let body: hcl::Body = hcl::from_reader(BufReader::new(file))?;
config Config::from_body(&body).map_err(|s| anyhow!("{}", s))?
}; };
for (jobname, job) in cfg.jobs { for (jobname, job) in cfg.jobs {
@ -88,7 +92,7 @@ impl Ctx {
self.add_diplonat_tag( self.add_diplonat_tag(
conf, conf,
&job.datacenters, &job.datacenters,
PortRegistration::Config { PortRegistration {
file: PathBuf::from(file), file: PathBuf::from(file),
job: jobname.clone(), job: jobname.clone(),
group: groupname.clone(), group: groupname.clone(),
@ -144,13 +148,6 @@ fn main() -> Result<ExitCode> {
} }
let mut ctx = Ctx::new(); let mut ctx = Ctx::new();
// port registrations managed externally and not described in .hcl files
ctx.ports.insert((String::from("bespin"), Port::Tcp(80)),
vec![PortRegistration::Builtin(String::from("forjego"))]);
ctx.ports.insert((String::from("bespin"), Port::Tcp(443)),
vec![PortRegistration::Builtin(String::from("forjego"))]);
for file in &args[1..] { for file in &args[1..] {
ctx.add_file(Path::new(file))? ctx.add_file(Path::new(file))?
} }
@ -161,7 +158,8 @@ fn main() -> Result<ExitCode> {
have_conflicts = true; have_conflicts = true;
println!("Conflict in site {}, port {:?}:", &datacenter, port); println!("Conflict in site {}, port {:?}:", &datacenter, port);
for reg in &regs { for reg in &regs {
println!("- {}", reg) println!("- in {}, job \"{}\", group \"{}\", task \"{}\", service \"{}\"",
reg.file.display(), reg.job, reg.group, reg.task, reg.service)
} }
println!() println!()
} }

View file

@ -1,10 +1,5 @@
use serde::de::{ use serde::Deserialize;
value::{MapAccessDeserializer, SeqAccessDeserializer}, use hcl::{Structure, Body, Map, Expression};
MapAccess, SeqAccess, Visitor,
};
use serde::{Deserialize, Deserializer};
use std::fmt;
use std::marker::PhantomData;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Config { pub struct Config {
@ -15,68 +10,140 @@ pub struct Config {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Job { pub struct Job {
pub datacenters: Vec<String>, pub datacenters: Vec<String>,
#[serde(rename = "group", default = "hcl::Map::new")] #[serde(rename = "group")]
pub groups: hcl::Map<String, Group>, pub groups: hcl::Map<String, Group>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Group { pub struct Group {
#[serde(rename = "task", default = "hcl::Map::new")] #[serde(rename = "task")]
pub tasks: hcl::Map<String, Task>, pub tasks: hcl::Map<String, Task>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Task { pub struct Task {
#[serde(rename = "service", deserialize_with = "vec_or_map", default = "Vec::new")] #[serde(rename = "service")]
pub services: Vec<Service>, pub services: Vec<Service>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Service { pub struct Service {
pub name: Option<String>, pub name: String,
#[serde(default = "Vec::new")]
pub tags: Vec<String>, pub tags: Vec<String>,
} }
// In .hcl files, a field can sometimes container either a record (for a single // lots of manual boilerplate because I can't figure out how to use the
// item), or a sequence of records (for several items). This is not something // automated deserialization for blocks without labels (e.g. "service") that may
// that the built-in hcl deserializer handles. Instead, the following // appear one or several times:
// deserializer can be attached to fields that follow this schema with the // https://github.com/martinohmann/hcl-rs/issues/380
// "deserialize_with" serde attribute.
//
// Thanks to the hcl-rs maintener for the code snippet
// (https://github.com/martinohmann/hcl-rs/issues/380#issuecomment-2456546232)
fn vec_or_map<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>,
{
struct VecOrMap<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for VecOrMap<T> impl Config {
where pub fn from_body(b: &Body) -> Result<Self, String> {
T: Deserialize<'de>, Ok(Config { jobs: blocks_from_body(b, "job", Job::from_body)? })
{ }
type Value = Vec<T>; }
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { impl Job {
formatter.write_str("sequence or map") 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))
} }
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
Deserialize::deserialize(SeqAccessDeserializer::new(seq))
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
Deserialize::deserialize(MapAccessDeserializer::new(map)).map(|value| vec![value])
}
}
deserializer.deserialize_any(VecOrMap(PhantomData))
} }