Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-matrix-ctl/src/main.rs

105 lines
3.7 KiB
Rust

//! `swarm-matrix-ctl` — the rust that runs *inside* `containers.hive-matrix`.
//!
//! One binary with subcommands rather than one binary per job. The container
//! is an awkward place to put code — it needs its own store identity, its own
//! bind mounts and its own cert role — and all of that is per-*container*, not
//! per-task. A second single-purpose crate would have had to duplicate the
//! identity plumbing to add one action, so the next thing that has to run in
//! here is a verb below, not a new crate.
//!
//! [`appservice`] mints the **swarm's** own appservice registration and
//! keeps it in step with the store and publishes its token for `swarm-controller`.
//!
//! 🩸 **A secret is a path, never a value.** The only identifier any verb here
//! logs is the store path; see `swarm_matrix_client`'s module doc for the same rule
//! applied to error messages.
mod appservice;
use anyhow::Result;
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(
name = "swarm-matrix-ctl",
about = "Act on the swarm's matrix homeserver from inside its container"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
/// The swarm's own appservice registration, whose sender is the
/// homeserver's admin account. Configured by `MATRIX_APPSERVICE_*`.
#[command(subcommand)]
Appservice(Appservice),
}
#[derive(Debug, Subcommand)]
enum Appservice {
/// Mint the tokens when absent and render the registration tuwunel loads.
/// Local only: it runs before the homeserver and must not need a network.
Render,
/// Make the state dir hold the store's registration, seeding the store
/// from it when the store holds none, and restart the homeserver when the
/// registration is newer than its last start. Runs beside the homeserver, never before it.
Sync,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
match Cli::parse().command {
Command::Appservice(Appservice::Render) => appservice::render(),
Command::Appservice(Appservice::Sync) => appservice::sync().await,
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn the_clap_tree_is_well_formed() {
Cli::command().debug_assert();
}
/// The two units name these verbs in `ExecStart`, so a rename of one is a
/// deploy-time failure with no local signal. This is that signal.
#[test]
fn the_appservice_verbs_are_spelled_the_way_the_units_invoke_them() {
let cli =
Cli::try_parse_from(["swarm-matrix-ctl", "appservice", "render"]).expect("a verb");
assert!(matches!(
cli.command,
Command::Appservice(Appservice::Render)
));
let cli = Cli::try_parse_from(["swarm-matrix-ctl", "appservice", "sync"]).expect("a verb");
assert!(matches!(cli.command, Command::Appservice(Appservice::Sync)));
Cli::try_parse_from(["swarm-matrix-ctl", "appservice"])
.expect_err("a sub-verb is required");
}
#[test]
fn an_unknown_verb_is_refused() {
Cli::try_parse_from(["swarm-matrix-ctl", "conjure"])
.expect_err("only declared verbs are accepted");
}
/// A bare invocation must not silently do something. `appservice sync`
/// writes a credential, so "no verb" defaulting to it would make a typo in
/// the unit write rather than fail.
#[test]
fn no_verb_at_all_is_refused() {
Cli::try_parse_from(["swarm-matrix-ctl"]).expect_err("a verb is required");
}
}