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");
}
}