Compare commits
No commits in common. "main@origin" and "main" have entirely different histories.
main@origi
...
main
2 changed files with 87 additions and 152 deletions
66
src/main.rs
66
src/main.rs
|
@ -1,37 +1,14 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{env,fs::File};
|
use std::{env,fmt,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::{anyhow, Result};
|
use 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)]
|
||||||
|
@ -60,12 +37,31 @@ 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)]
|
||||||
struct PortRegistration {
|
enum PortRegistration {
|
||||||
|
Config {
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
job: String,
|
job: String,
|
||||||
group: String,
|
group: String,
|
||||||
task: String,
|
task: String,
|
||||||
service: String,
|
service: Option<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 {
|
||||||
|
@ -77,8 +73,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 body: hcl::Body = hcl::from_reader(BufReader::new(file))?;
|
let config: Config = hcl::from_reader(BufReader::new(file))?;
|
||||||
Config::from_body(&body).map_err(|s| anyhow!("{}", s))?
|
config
|
||||||
};
|
};
|
||||||
|
|
||||||
for (jobname, job) in cfg.jobs {
|
for (jobname, job) in cfg.jobs {
|
||||||
|
@ -92,7 +88,7 @@ impl Ctx {
|
||||||
self.add_diplonat_tag(
|
self.add_diplonat_tag(
|
||||||
conf,
|
conf,
|
||||||
&job.datacenters,
|
&job.datacenters,
|
||||||
PortRegistration {
|
PortRegistration::Config {
|
||||||
file: PathBuf::from(file),
|
file: PathBuf::from(file),
|
||||||
job: jobname.clone(),
|
job: jobname.clone(),
|
||||||
group: groupname.clone(),
|
group: groupname.clone(),
|
||||||
|
@ -148,6 +144,13 @@ 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))?
|
||||||
}
|
}
|
||||||
|
@ -158,8 +161,7 @@ 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 ®s {
|
for reg in ®s {
|
||||||
println!("- in {}, job \"{}\", group \"{}\", task \"{}\", service \"{}\"",
|
println!("- {}", reg)
|
||||||
reg.file.display(), reg.job, reg.group, reg.task, reg.service)
|
|
||||||
}
|
}
|
||||||
println!()
|
println!()
|
||||||
}
|
}
|
||||||
|
|
155
src/parsing.rs
155
src/parsing.rs
|
@ -1,5 +1,10 @@
|
||||||
use serde::Deserialize;
|
use serde::de::{
|
||||||
use hcl::{Structure, Body, Map, Expression};
|
value::{MapAccessDeserializer, SeqAccessDeserializer},
|
||||||
|
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 {
|
||||||
|
@ -10,140 +15,68 @@ 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")]
|
#[serde(rename = "group", default = "hcl::Map::new")]
|
||||||
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")]
|
#[serde(rename = "task", default = "hcl::Map::new")]
|
||||||
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")]
|
#[serde(rename = "service", deserialize_with = "vec_or_map", default = "Vec::new")]
|
||||||
pub services: Vec<Service>,
|
pub services: Vec<Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
pub name: String,
|
pub name: Option<String>,
|
||||||
|
#[serde(default = "Vec::new")]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// lots of manual boilerplate because I can't figure out how to use the
|
// In .hcl files, a field can sometimes container either a record (for a single
|
||||||
// automated deserialization for blocks without labels (e.g. "service") that may
|
// item), or a sequence of records (for several items). This is not something
|
||||||
// appear one or several times:
|
// that the built-in hcl deserializer handles. Instead, the following
|
||||||
// https://github.com/martinohmann/hcl-rs/issues/380
|
// deserializer can be attached to fields that follow this schema with the
|
||||||
|
// "deserialize_with" serde attribute.
|
||||||
impl Config {
|
//
|
||||||
pub fn from_body(b: &Body) -> Result<Self, String> {
|
// Thanks to the hcl-rs maintener for the code snippet
|
||||||
Ok(Config { jobs: blocks_from_body(b, "job", Job::from_body)? })
|
// (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>,
|
||||||
impl Job {
|
D: Deserializer<'de>,
|
||||||
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();
|
struct VecOrMap<T>(PhantomData<fn() -> T>);
|
||||||
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>
|
impl<'de, T> Visitor<'de> for VecOrMap<T>
|
||||||
where F: Fn(&Body) -> Result<T, String>
|
where
|
||||||
{
|
T: Deserialize<'de>,
|
||||||
let mut blocks = Vec::new();
|
{
|
||||||
for s in &b.0 {
|
type Value = Vec<T>;
|
||||||
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>
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
{
|
formatter.write_str("sequence or map")
|
||||||
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>
|
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||||
{
|
where
|
||||||
match e {
|
A: SeqAccess<'de>,
|
||||||
Expression::String(s) => Ok(s.to_string()),
|
{
|
||||||
_ => Err(format!("string expected, got {:?}", &e)),
|
Deserialize::deserialize(SeqAccessDeserializer::new(seq))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn tags_from_expr(e: &Expression) -> Result<Vec<String>, String>
|
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
|
||||||
{
|
where
|
||||||
match e {
|
M: MapAccess<'de>,
|
||||||
Expression::Array(es) => {
|
{
|
||||||
Ok(es.into_iter().filter_map(|e| match string_from_expr(e) {
|
Deserialize::deserialize(MapAccessDeserializer::new(map)).map(|value| vec![value])
|
||||||
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>
|
deserializer.deserialize_any(VecOrMap(PhantomData))
|
||||||
{
|
|
||||||
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