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

nordisk/monetize

crates/lake-product/src/installer.rs

//! **The bench's installer**: the ONE box's `Installer` seam, filled for the in-process Lake
//! bench. The core (`estate_order`) calls it after the lay and before the capture. For each
//! machine the lay made it starts that machine's [`LakeBox`] (in this process, on loopback),
//! feeds it (`POST /pair`) and reads back its offer (its fleet key, its ticket key).
//!
//! A DOUBLE, said: the real installer is `monetize-shop-pairing` (nordisk-pairing over each
//! box's WireGuard link, mTLS), which the fast test (`crates/monetize-fast`) runs. It cannot
//! pair a Lake yet: nordisk-pairing's seed kinds are Front, Gunnar and Holger (P3: one box kind
//! per role). What is called, when, and what must be answered is the same.

use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};

use monetize_server::estate_order::{
    self, update, FirstDoor, InstallRequest, InstallVerdict, Installer, LaidOrder, PairCheck,
    SaleCheckAt, VERDICT_FILE,
};

use crate::lake_box::{role, LakeBox};
use crate::{http, say};

/// The estate's boxes, by role, as the installer started them.
#[derive(Clone, Default)]
pub struct Boxes(pub Arc<Mutex<BTreeMap<String, Arc<LakeBox>>>>);

impl Boxes {
    pub fn all(&self) -> Vec<Arc<LakeBox>> {
        self.0.lock().unwrap().values().cloned().collect()
    }
    pub fn get(&self, role: &str) -> Option<Arc<LakeBox>> {
        self.0.lock().unwrap().get(role).cloned()
    }
    pub fn roles(&self) -> Vec<String> {
        self.0.lock().unwrap().keys().cloned().collect()
    }
    fn front(&self) -> Result<Arc<LakeBox>, String> {
        self.get(role::FRONT)
            .ok_or_else(|| "the Lake has no front box yet".to_string())
    }
}

pub struct LakeInstaller {
    pub boxes: Boxes,
}

fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

impl Installer for LakeInstaller {
    /// **INSTALL** (after the lay): one box per laid machine, each fed and its offer read back.
    /// The fleet keys the boxes offer are NAMED in the verdict, so no box is pinned by whoever
    /// calls first; the ticket key is the front's (it mints the buyer's tickets).
    fn install(&self, req: &InstallRequest) -> Result<InstallVerdict, String> {
        say(
            "installer",
            "install",
            "monetize-server",
            &format!(
                "product={} company={} account={}",
                req.facts.kind, req.company, req.account
            ),
        );
        let order: LaidOrder = serde_json::from_slice(
            &std::fs::read(&req.laid).map_err(|e| format!("{}: {e}", req.laid.display()))?,
        )
        .map_err(|e| e.to_string())?;
        let mut keys = BTreeMap::new();
        let mut ticket_key = String::new();
        for m in &order.estate.machines {
            let b = match self.boxes.get(&m.role) {
                Some(b) => b,
                None => {
                    let b = LakeBox::start(&m.role)
                        .map_err(|e| format!("the {} box did not start: {e}", m.role))?;
                    self.boxes
                        .0
                        .lock()
                        .unwrap()
                        .insert(m.role.clone(), b.clone());
                    b
                }
            };
            let feed = serde_json::json!({ "company": order.company, "role": m.role, "fleet_instance": req.company, "monetize_pubkey": order.monetize_pubkey, "shop_door": order.shop_door });
            let offer: serde_json::Value =
                serde_json::from_str(&http::ok(b.addr, "POST", "/pair", &feed.to_string())?)
                    .map_err(|e| e.to_string())?;
            keys.insert(
                m.role.clone(),
                offer["fleet_pubkey"]
                    .as_str()
                    .unwrap_or_default()
                    .to_string(),
            );
            if m.role == role::FRONT {
                if let Some(t) = offer["ticket_key"]
                    .as_str()
                    .and_then(monetize_embed::signing::base64_decode)
                {
                    ticket_key = hex(&t);
                }
            }
        }
        let v = InstallVerdict {
            verdict: "PASS".into(),
            detail: format!(
                "the bench installer fed {} Lake box(es)",
                order.estate.machines.len()
            ),
            receipt: serde_json::json!({ "verb": "lake-install", "verdict": "PASS" }),
            peers: Vec::new(),
            appliance_keys: keys,
            ticket_key,
        };
        std::fs::write(
            req.run.join(VERDICT_FILE),
            serde_json::to_vec(&v).map_err(|e| e.to_string())?,
        )
        .map_err(|e| e.to_string())?;
        Ok(v)
    }

    /// **SALE CHECK** (before the capture): the front answers its login page and its first-admin
    /// door is open.
    fn sale_check(&self, at: &SaleCheckAt<'_>) -> Result<String, String> {
        say(
            "installer",
            "sale_check",
            "monetize-server",
            &format!("host={} account={}", at.host, at.account),
        );
        http::ok(self.boxes.front()?.addr, "GET", "/login", "")?;
        match self.first_door(at)? {
            FirstDoor::Open | FirstDoor::Spent => Ok(format!(
                "the Lake front of {} serves its login page and its first-admin door",
                at.company
            )),
            FirstDoor::Closed(w) => Err(format!("the Lake's first-admin door is closed: {w}")),
        }
    }

    fn first_door(&self, at: &SaleCheckAt<'_>) -> Result<FirstDoor, String> {
        say(
            "installer",
            "first_door",
            "monetize-server",
            &format!("host={}", at.host),
        );
        let j: serde_json::Value = serde_json::from_str(&http::ok(
            self.boxes.front()?.addr,
            "GET",
            "/first-login",
            "",
        )?)
        .map_err(|e| e.to_string())?;
        Ok(match j["door"].as_str().unwrap_or_default() {
            "open" => FirstDoor::Open,
            "spent" => FirstDoor::Spent,
            other => FirstDoor::Closed(other.to_string()),
        })
    }

    fn pair_status(&self, req: &InstallRequest) -> Result<PairCheck, String> {
        say(
            "installer",
            "pair_status",
            "monetize-server",
            &format!("company={}", req.company),
        );
        let boxes = self
            .boxes
            .all()
            .into_iter()
            .map(|b| {
                (
                    b.role.clone(),
                    http::ok(b.addr, "GET", "/login", "").map(|_| "PAIRED".to_string()),
                )
            })
            .collect();
        Ok(PairCheck {
            boxes,
            ticket_key: String::new(),
        })
    }

    /// **LIVE DOOR**: `GET /live/running` on the box of `role`.
    fn live_running(
        &self,
        req: &InstallRequest,
        role: &str,
    ) -> Result<update::LiveRunning, String> {
        say(
            "installer",
            "live_running",
            "monetize-server",
            &format!("company={} role={role}", req.company),
        );
        let b = self
            .boxes
            .get(role)
            .ok_or_else(|| format!("no Lake box {role}"))?;
        let j: serde_json::Value =
            serde_json::from_str(&http::ok(b.addr, "GET", "/live/running", "")?)
                .map_err(|e| e.to_string())?;
        Ok(update::LiveRunning {
            base: j["base"].as_str().unwrap_or_default().into(),
            app: j["app"].as_str().unwrap_or_default().into(),
            slot: j["slot"].as_str().unwrap_or_default().into(),
            live_update: j["live_update"].as_bool().unwrap_or(false),
        })
    }

    /// **LIVE DOOR**: `POST /live/stage` with the release's pin, on the box of `role`.
    fn live_stage(
        &self,
        req: &InstallRequest,
        role: &str,
        stage: &update::LiveStage,
    ) -> Result<String, String> {
        say(
            "installer",
            "live_stage",
            "monetize-server",
            &format!("company={} role={role} pin={}", req.company, stage.pin_hex),
        );
        let b = self
            .boxes
            .get(role)
            .ok_or_else(|| format!("no Lake box {role}"))?;
        http::ok(
            b.addr,
            "POST",
            "/live/stage",
            &serde_json::json!({ "pin_hex": stage.pin_hex }).to_string(),
        )
    }
}

/// The estate orders of a bench ONE box, filled with the bench's installer over `boxes`.
pub fn estate_orders(
    cfg: estate_order::EstateOrderConfig,
    dir: std::path::PathBuf,
    boxes: Boxes,
    custody: monetize_server::custody::Custody,
) -> estate_order::EstateOrders {
    estate_order::EstateOrders::with(
        cfg,
        dir,
        std::sync::Arc::new(LakeInstaller { boxes }),
        std::sync::Arc::new(crate::bench::LakeDns),
        custody,
    )
}

#[cfg(test)]
mod tests {
    #[test]
    fn hex_is_two_lower_digits_per_byte() {
        assert_eq!(super::hex(&[]), "");
        assert_eq!(super::hex(&[0x00, 0x0a, 0xff]), "000aff");
    }
}