//! `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. //! //! Today that is one verb, [`mint`]: publish the appservice sender account's //! homeserver access token to the swarm's secret store, once. //! //! 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 `homeserver`'s module doc for the same rule //! applied to error messages. mod homeserver; 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, } #[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, } } #[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. #[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"); } }