mannequin/src/main.rs
use anyhow::Result;
use clap::{Parser, Subcommand};
use mannequin::{MannequinConfig, MannequinServer};
#[derive(Parser)]
#[command(name = "holger-mannequin")]
#[command(about = "Test dummy: PKI, OIDC, mock registries for holger development")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Start the mannequin daemon (all services)
Serve {
#[arg(short, long, default_value = "9999")]
port: u16,
/// Open the egui system map dashboard (needs the `gui` feature)
#[arg(long)]
gui: bool,
/// Config file (RON). If omitted, generates fresh certs.
#[arg(short, long)]
config: Option<std::path::PathBuf>,
},
/// PKI operations (standalone, no daemon needed)
Pki {
#[command(subcommand)]
action: PkiAction,
},
#[cfg(feature = "gui")]
/// Robot-drive the mannequin GUI through its starter scenario. Default: HEADLESS
/// (off-screen egui_kittest → CI matrix rows). `--watch` opens a REAL on-screen
/// eframe window and replays the scenario, paced + visible. Env `ROBOTUI_WATCH=1`
/// also selects watch; `ROBOTUI_WATCH_DELAY_MS` / `ROBOTUI_WATCH_HOLD=1` tune it.
Robot {
/// Open a real window and replay visibly (RunMode::Watch).
#[arg(long)]
watch: bool,
},
}
#[derive(Subcommand)]
enum PkiAction {
/// Generate CA + server cert to a directory
Generate {
#[arg(short, long, default_value = "./certs")]
output: String,
#[arg(long, default_value = "localhost")]
cn: String,
#[arg(long, value_delimiter = ',')]
sans: Vec<String>,
},
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
match cli.command {
Commands::Serve { port, gui, config } => {
let mannequin_config = if let Some(cfg_path) = config {
let mut cfg = MannequinConfig::load(&cfg_path)?;
cfg.port = port;
cfg
} else {
MannequinConfig {
port,
..Default::default()
}
};
let server = MannequinServer::from_config(mannequin_config)?;
println!("Holger Mannequin starting on port {}", port);
println!(" PKI: http://127.0.0.1:{}/pki/ca.pem", port);
println!(
" OIDC: http://127.0.0.1:{}/.well-known/openid-configuration",
port
);
println!(
" Nexus: http://127.0.0.1:{}/service/rest/v1/repositories",
port
);
#[cfg(not(feature = "gui"))]
if gui {
anyhow::bail!("this mannequin was built without the `gui` feature");
}
#[cfg(feature = "gui")]
if gui {
let event_log = server.event_log.clone();
let pki = server.pki.clone();
// Spawn the HTTP server in a background task
tokio::spawn(async move {
server.serve().await.unwrap();
});
// Run GUI on main thread (eframe requirement)
let gui_app = mannequin::MannequinGui::new(event_log, pki);
gui_app.run()?;
return Ok(());
}
server.serve().await?;
}
#[cfg(feature = "gui")]
Commands::Robot { watch } => {
use mannequin::robot;
use nornir_robotui::{status, RunMode, RunOutcome, WatchConfig};
let mode = if watch || RunMode::from_env().is_watch() {
RunMode::Watch
} else {
RunMode::Headless
};
let cfg = WatchConfig::from_env().with_title("Holger Mannequin — Robot");
match robot::run(mode, cfg)? {
RunOutcome::Headless(rows) => {
println!(
"mannequin robot matrix — suite `mannequin-robot` ({} rows)",
rows.len()
);
for r in &rows {
let mark = if r.status == status::PASS {
"PASS"
} else {
"FAIL"
};
print!(" [{mark}] {}", r.test_name);
if !r.message.is_empty() {
print!(" — {}", r.message);
}
println!();
}
let passed = rows.iter().filter(|r| r.status == status::PASS).count();
println!(" {passed}/{} passed", rows.len());
if passed != rows.len() {
anyhow::bail!("mannequin robot scenario had failures");
}
}
RunOutcome::Watched => {
println!("mannequin robot: watch window closed");
}
}
}
Commands::Pki { action } => match action {
PkiAction::Generate { output, cn, sans } => {
use mannequin::PkiService;
let pki = PkiService::new()?;
std::fs::create_dir_all(&output)?;
let sans_refs: Vec<&str> = sans.iter().map(|s| s.as_str()).collect();
let server_cert = pki.issue_server_cert(&cn, sans_refs)?;
let ca_path = format!("{}/ca.pem", output);
let cert_path = format!("{}/server.pem", output);
let key_path = format!("{}/server-key.pem", output);
std::fs::write(&ca_path, pki.ca_cert_pem())?;
std::fs::write(&cert_path, &server_cert.cert_pem)?;
std::fs::write(&key_path, &server_cert.key_pem)?;
println!("Written to {}:", output);
println!(" {}", ca_path);
println!(" {}", cert_path);
println!(" {}", key_path);
}
},
}
Ok(())
}