Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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(())
}