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

nordisk/gunnar

crates/gunnar-repair/src/adopt.rs

//! **`adopt_twin`**: certify a twin for its primary — moved here from the pairing
//! door (`gunnar-pairing-door` `certify::once`, 2026-09-24), which now calls this.
//! The twin's key, enrolled on the primary as [`TWIN_PRINCIPAL`] in group `twin`,
//! then its TLS certificate: `Bootstrap.TwinCsr` on the twin,
//! `Admin.IssueTwinCertificate` on the primary, `Bootstrap.InstallTwinCertificate`
//! on the twin. The private key crosses no call; every step is idempotent, so a
//! courier that dies half-way is simply run again.

use serde::{Deserialize, Serialize};

/// The principal the twin is enrolled as on its primary — the CN its replication
/// request carries.
pub const TWIN_PRINCIPAL: &str = "gunnar-twin";

/// What `Admin.IssueTwinCertificate` answers.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Issued {
    pub cert_pem: String,
    pub issuer_pem: String,
    pub port: u32,
    /// The address the primary's replication listener is bound on (its tunnel
    /// address), empty when it is the joined one.
    pub host: String,
}

/// **The calls, one per step** — tonic on the estate, a fake in the tests.
pub trait Boxes {
    /// `Bootstrap.BecomeTwin` on the twin: its public key line.
    fn twin_key(&self, twin_ctl: &str, primary_for_twin: &str) -> Result<String, String>;
    /// On the primary, signed with an admin key: `principal` as a machine with `key`
    /// — and ONLY that ssh key: one twin per principal, so a dead twin's key is
    /// revoked — group `twin`, `principal` a member. "Already there" is fine.
    fn enrol(&self, primary_ctl: &str, principal: &str, key: &str) -> Result<(), String>;
    /// `Bootstrap.TwinCsr` on the twin.
    fn twin_csr(&self, twin_ctl: &str) -> Result<String, String>;
    /// `Admin.IssueTwinCertificate` on the primary, signed with an admin key.
    fn issue(&self, primary_ctl: &str, principal: &str, csr_pem: &str) -> Result<Issued, String>;
    /// `Bootstrap.InstallTwinCertificate` on the twin: days of certificate left.
    fn install(&self, twin_ctl: &str, issued: &Issued) -> Result<i64, String>;
}

/// Which twin, for which primary.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Adopt {
    /// The twin's control plane (`<addr>:50051`).
    pub twin_ctl: String,
    /// The primary's control plane.
    pub primary_ctl: String,
    /// The primary as the TWIN was told it — `BecomeTwin` is idempotent only for
    /// the same string, and a twin is never re-pointed.
    pub primary_for_twin: String,
    /// Empty = [`TWIN_PRINCIPAL`].
    #[serde(default)]
    pub principal: String,
}

/// A successful adoption.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Paired {
    /// Where the twin now follows over mutual TLS: `host:port`.
    pub replication: String,
    pub days_left: i64,
}

/// **One attempt, every step.** `Err` names the step it stopped at.
pub fn adopt_twin(boxes: &dyn Boxes, t: &Adopt) -> Result<Paired, String> {
    let principal = if t.principal.trim().is_empty() {
        TWIN_PRINCIPAL
    } else {
        t.principal.trim()
    };
    let key = boxes
        .twin_key(&t.twin_ctl, &t.primary_for_twin)
        .map_err(|e| format!("twin-key: the twin at {} gave no key: {e}", t.twin_ctl))?;
    let key = key.trim();
    if !key.starts_with("ssh-") {
        return Err(format!(
            "twin-key: the twin at {} answered no public key line: `{key}`",
            t.twin_ctl
        ));
    }
    boxes.enrol(&t.primary_ctl, principal, key).map_err(|e| {
        format!(
            "enrol: the primary at {} would not enrol {principal}: {e}",
            t.primary_ctl
        )
    })?;
    let csr = boxes.twin_csr(&t.twin_ctl).map_err(|e| {
        format!(
            "twin-csr: the twin at {} would not give a signing request: {e}",
            t.twin_ctl
        )
    })?;
    let issued = boxes.issue(&t.primary_ctl, principal, &csr).map_err(|e| {
        format!(
            "issue: the primary at {} would not certify {principal}: {e}",
            t.primary_ctl
        )
    })?;
    let days_left = boxes.install(&t.twin_ctl, &issued).map_err(|e| {
        format!(
            "install: the twin at {} refused its certificate: {e}",
            t.twin_ctl
        )
    })?;
    let host = if issued.host.is_empty() {
        t.primary_ctl
            .rsplit_once(':')
            .map(|(h, _)| h.to_string())
            .unwrap_or_default()
    } else {
        issued.host.clone()
    };
    Ok(Paired {
        replication: format!("{host}:{}", issued.port),
        days_left,
    })
}