Files
Rayhaan JaufeerallyandClaude Opus 5.5 dc275f99d7
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / interop (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s
Add a documentation website, built with Zola
The documentation moves from READMEs into a site in docs/: getting
started, a user guide, operations, a reference, and development pages,
written in the style of network vendors' documentation (docs/STYLE.md).
bin/bgp/README.md and docs/RFCs.md are folded into it.

- A theme of its own in the International Typographic Style, in blues,
  with self-hosted Inter and JetBrains Mono, search over Zola's index,
  dark mode, and no resources loaded from other hosts.
- Example configurations in docs/examples/, shown on the pages and
  checked with `bgp --check` in CI.
- A test that fails when a field of config.proto is missing from the
  configuration reference.
- A CI job that builds the site; Zola fails on broken links and anchors.
- A hardened systemd unit, tried under systemd with kernel routes on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:12:43 +02:00

102 lines
3.7 KiB
Rust

//! The configuration reference on the documentation site describes every
//! field of the configuration's schema.
//!
//! The schema is `proto/config.proto`; the reference is
//! `docs/content/docs/reference/configuration.md`, with a section for each
//! message and enum (`## Peer`) that names each of its fields or values in
//! backquotes. A field added to the schema and not to the reference fails
//! here, so that operators can find every setting the daemon has.
use std::collections::BTreeMap;
use std::path::Path;
/// The messages and enums of a `.proto` file, each with its fields or
/// values, in the order they are declared.
fn declarations(proto: &str) -> BTreeMap<String, Vec<String>> {
let mut found = BTreeMap::new();
let mut current: Option<(String, Vec<String>)> = None;
for line in proto.lines() {
let line = line.trim();
if line.starts_with("//") || line.is_empty() {
continue;
}
let words: Vec<&str> = line.split_whitespace().collect();
match words.as_slice() {
["message" | "enum", name, "{"] => current = Some((name.to_string(), Vec::new())),
["}"] => {
if let Some((name, members)) = current.take() {
found.insert(name, members);
}
}
// `repeated Peer peers = 4;`, `string name = 1;`, `PEER = 3;`
_ => {
let Some((_, members)) = current.as_mut() else {
continue;
};
let Some(at) = words.iter().position(|word| *word == "=") else {
continue;
};
let name = words[at - 1];
// An enum's zero value is what is left out; there is
// nothing to configure.
if !name.ends_with("_UNSPECIFIED") {
members.push(name.to_string());
}
}
}
}
found
}
/// The text of each `## ` section of a Markdown page, by its heading.
fn sections(page: &str) -> BTreeMap<String, String> {
let mut found = BTreeMap::new();
let mut current: Option<(String, String)> = None;
for line in page.lines() {
if let Some(heading) = line.strip_prefix("## ") {
if let Some((name, text)) = current.take() {
found.insert(name, text);
}
current = Some((heading.trim().to_string(), String::new()));
} else if let Some((_, text)) = current.as_mut() {
text.push_str(line);
text.push('\n');
}
}
if let Some((name, text)) = current {
found.insert(name, text);
}
found
}
#[test]
fn the_configuration_reference_has_every_field() {
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let proto = std::fs::read_to_string(crate_dir.join("proto/config.proto")).unwrap();
let reference_path = crate_dir.join("../../docs/content/docs/reference/configuration.md");
let reference = std::fs::read_to_string(&reference_path).unwrap();
let declared = declarations(&proto);
assert!(declared.contains_key("BgpConfig") && declared.contains_key("Peer"));
let documented = sections(&reference);
let mut missing = Vec::new();
for (name, members) in &declared {
let Some(section) = documented.get(name) else {
missing.push(format!("a section `## {name}`"));
continue;
};
for member in members {
if !section.contains(&format!("`{member}`")) {
missing.push(format!("`{member}` under `## {name}`"));
}
}
}
assert!(
missing.is_empty(),
"{} does not describe:\n {}",
reference_path.display(),
missing.join("\n ")
);
}