//! `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 //! 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; 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 { /// 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::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 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", "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. `appservice publish` /// 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"); } }