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>
This commit is contained in:
1 parent
9820a5d3bc
commit
dc275f99d7
61 files changed
+5632
-623
No files matched your search
@@ -0,0 +1,101 @@
|
||||
//! 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 ")
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user