//! `swarmctl forge make-admin` — make an existing human forge account a site //! admin, through the swarm-controller over the forge's admin API. //! //! Unlike the `agent` verbs this one waits: the controller answers with what //! it found. Same round trip as [`crate::agent`], over the controller's unix //! socket; the shape below mirrors the controller's own private //! `MakeForgeAdminResponse` for the reason that module's doc comment gives. use std::path::Path; use anyhow::{Context as _, Result}; use serde::Deserialize; use crate::agent::{parse_ident, post}; /// Success body of `POST /api/forge/users/{name}/admin`. #[derive(Deserialize)] struct MakeForgeAdminResponse { change: AdminChange, } #[derive(Deserialize)] #[serde(rename_all = "snake_case")] enum AdminChange { Promoted, AlreadyAdmin, } /// Run `swarmctl forge make-admin`. /// /// Synchronous for the same reason `agent create` is. pub(crate) fn make_admin(socket: &Path, name: &str) -> Result<()> { let name = parse_ident(name, "user name")?; let rt = tokio::runtime::Builder::new_current_thread() .enable_io() .build() .context("starting a tokio runtime for the controller request")?; let resp: MakeForgeAdminResponse = rt.block_on(post( socket, &format!("/api/forge/users/{name}/admin"), &serde_json::json!({}), "forge-make-admin", ))?; match resp.change { AdminChange::Promoted => println!("forge: {name:?} is now a site admin"), AdminChange::AlreadyAdmin => println!("forge: {name:?} was already a site admin"), } Ok(()) } #[cfg(test)] mod tests { use super::{AdminChange, MakeForgeAdminResponse}; /// The controller's two answers, as it spells them. #[test] fn both_changes_decode() { for (wire, want) in [("promoted", true), ("already_admin", false)] { let resp: MakeForgeAdminResponse = serde_json::from_str(&format!(r#"{{"change":"{wire}"}}"#)).expect("decodes"); assert_eq!(matches!(resp.change, AdminChange::Promoted), want); } } }