//! `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. //! //! [`mint`] publishes a hive's appservice sender token to the swarm's secret //! store, once. [`appservice`] mints the **swarm's** own appservice //! registration and publishes its token for `swarm-controller`. //! //! It lives in the container because the appservice `as_token` that authorises //! the mint is *already* there — the registration tuwunel loads is bind-mounted //! in — so no second holder of that secret is created. //! //! 🩸 **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; mod mint; mod registration; 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 { /// Publish the appservice sender account's access token to the swarm /// secret store, once. /// /// Configured entirely by the `MATRIX_MINT_*` environment the unit sets — /// no flags, because a systemd `Environment=` block is what a nix module /// can render and a command line full of paths is not. Mint, /// 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, /// Write the rendered `as_token` to the swarm secret store when the /// store's copy differs. Publish, } #[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::Mint => mint::run().await, Command::Appservice(Appservice::Render) => appservice::render(), Command::Appservice(Appservice::Publish) => appservice::publish().await, } } #[cfg(test)] mod tests { use super::*; use clap::CommandFactory; #[test] fn the_clap_tree_is_well_formed() { Cli::command().debug_assert(); } /// The unit's `ExecStart` names a verb, so a rename of it is a deploy-time /// failure with no local signal. This is that signal. #[test] fn mint_is_spelled_the_way_the_unit_invokes_it() { let cli = Cli::try_parse_from(["swarm-matrix-ctl", "mint"]).expect("`mint` is a verb"); assert!(matches!(cli.command, Command::Mint)); } /// The control: without it the case above passes on a parser that accepts /// anything. /// The two units name these verbs, same reason as the test above. #[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", "publish"]).expect("a verb"); assert!(matches!( cli.command, Command::Appservice(Appservice::Publish) )); 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. `mint` writes a /// credential, so "no verb" defaulting to it would make a typo in the unit /// mint rather than fail. #[test] fn no_verb_at_all_is_refused() { Cli::try_parse_from(["swarm-matrix-ctl"]).expect_err("a verb is required"); } }