kcode-k1-loom-bootstrap 0.3.0

First-run Loom administrator, public topology, Rust source, and Web UI bootstrap
Documentation
#![doc = include_str!("../Documentation.md")]
#![forbid(unsafe_code)]

use kcode_k1_access::K1Access;
use kcode_k1_access_kmap::K1AccessKmap;
use kcode_k1_access_launch_nodes::{ModelId, TxId, UserId};
use kcode_k1_access_profiles::K1AccessProfiles;
use kcode_k1_bootstrap_identity::{BootstrapIdentity, prompt};
use kcode_k1_bootstrap_state::{
    BeginRecord, BootstrapStatus, CompleteRecord, ImportedPackage as StatePackage, K1BootstrapState,
};
use kcode_k1_groups::K1Groups;
use kcode_k1_invites::K1Invites;
use kcode_k1_launch_nodes::LaunchNodes;
use kcode_k1_loom_bootstrap_topology::{
    BootstrapTopology, TopologyServices, ensure as ensure_topology,
};
use kcode_k1_rust_bootstrap_import::RustBootstrapImporter;
use kcode_k1_rust_projection::K1RustProjection;
use kcode_k1_users::{K1Users, NewUser, User};
use kcode_k1_web_bootstrap_archive::read as read_web;
use kcode_k1_web_bootstrap_import::{
    ImportedPackage as WebImportedPackage, WebBootstrapImporter, write_preflight_log,
};
use std::path::Path;

const WEB_INVENTORY_PREFIX: &str = "web:";
const WEB_IMPORT_LOG: &str = "bootstrap/k1-web-import.log";
const BLANK_RESTART: &str = "Loom bootstrap previously began without completing; delete the disposable blank-state data and restart";

pub struct BootstrapServices<'a> {
    pub state: &'a K1BootstrapState,
    pub invites: &'a K1Invites,
    pub users: &'a K1Users,
    pub groups: &'a K1Groups,
    pub profiles: &'a K1AccessProfiles,
    pub access_kmap: &'a K1AccessKmap,
    pub access_launch_nodes: &'a kcode_k1_access_launch_nodes::K1AccessLaunchNodes,
    pub launch_nodes: &'a LaunchNodes,
    pub rust_projection: &'a K1RustProjection,
    pub model: ModelId,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BootstrapDeveloperGroups {
    loom_devs: TxId,
    kennedy_devs: TxId,
}

impl BootstrapDeveloperGroups {
    pub const fn loom_devs(&self) -> TxId {
        self.loom_devs
    }

    pub const fn kennedy_devs(&self) -> TxId {
        self.kennedy_devs
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BootstrapResult {
    record: CompleteRecord,
    completed_now: bool,
    developer_groups: BootstrapDeveloperGroups,
}

impl BootstrapResult {
    pub fn record(&self) -> &CompleteRecord {
        &self.record
    }

    pub const fn completed_now(&self) -> bool {
        self.completed_now
    }

    pub const fn developer_groups(&self) -> BootstrapDeveloperGroups {
        self.developer_groups
    }
}

pub fn ensure_with_topology(
    root: &Path,
    access: &K1Access,
    web_importer: &WebBootstrapImporter,
    services: BootstrapServices<'_>,
) -> Result<BootstrapResult, String> {
    match services.state.status()? {
        BootstrapStatus::Complete { record, .. } => {
            require_web_complete(&record)?;
            let first_user = UserId::from_tx_id(TxId::from_bytes(record.user_id()));
            let topology = reconcile_topology(first_user, access, &services)?;
            require_record_topology(&record, &topology)?;
            return Ok(result(record, false, &topology));
        }
        BootstrapStatus::Begun { .. } => return Err(BLANK_RESTART.to_owned()),
        BootstrapStatus::Empty => {}
    }

    let rust_importer = RustBootstrapImporter::open(
        &root.join("bootstrap/k1-rust-code.zip"),
        &root.join("bootstrap/k1-rust-import.log"),
    )?;
    let web = read_web(&root.join("bootstrap/k1-web-ui.zip"))?;
    let web_import_log = root.join(WEB_IMPORT_LOG);
    write_preflight_log(&web.archive, &web_import_log)?;

    let identity = prompt(kcode_k1_terms::text().as_bytes())?;
    let begin = BeginRecord::new(
        identity.username().to_owned(),
        identity.full_name().to_owned(),
        identity.public_key(),
    );
    services.state.begin(begin.clone())?;

    let user = reconcile_user(services.invites, services.users, &identity)?;
    let account_user_bytes = *user.user_id().as_tx_id().as_bytes();
    let first_user = UserId::from_tx_id(TxId::from_bytes(account_user_bytes));
    let topology = reconcile_topology(first_user, access, &services)?;
    let loom_authority = *topology.loom_group().txid().as_bytes();

    let imported_rust = rust_importer.import_all(services.rust_projection, loom_authority, root)?;
    let imported_web = web_importer.import_all(
        &web.archive,
        loom_authority,
        account_user_bytes,
        &web_import_log,
    )?;

    let mut inventory = rust_state_inventory(imported_rust)?;
    inventory.extend(web_state_inventory(imported_web)?);
    let record = CompleteRecord::new(
        begin,
        account_user_bytes,
        loom_authority,
        *topology.loom_profile().txid().as_bytes(),
        *topology.loom_root().txid().as_bytes(),
        inventory,
    )?;
    services.state.complete(record.clone())?;
    Ok(result(record, true, &topology))
}

fn result(
    record: CompleteRecord,
    completed_now: bool,
    topology: &BootstrapTopology,
) -> BootstrapResult {
    BootstrapResult {
        record,
        completed_now,
        developer_groups: BootstrapDeveloperGroups {
            loom_devs: TxId::from_bytes(*topology.loom_group().txid().as_bytes()),
            kennedy_devs: TxId::from_bytes(*topology.kennedy_group().txid().as_bytes()),
        },
    }
}

fn reconcile_topology(
    first_user: UserId,
    access: &K1Access,
    services: &BootstrapServices<'_>,
) -> Result<BootstrapTopology, String> {
    ensure_topology(
        first_user,
        TopologyServices {
            groups: services.groups,
            profiles: services.profiles,
            access,
            access_kmap: services.access_kmap,
            access_launch_nodes: services.access_launch_nodes,
            launch_nodes: services.launch_nodes,
            model: services.model,
        },
    )
}

fn require_record_topology(
    record: &CompleteRecord,
    topology: &BootstrapTopology,
) -> Result<(), String> {
    if record.group_id() == *topology.loom_group().txid().as_bytes()
        && record.profile_id() == *topology.loom_profile().txid().as_bytes()
        && record.root_id() == *topology.loom_root().txid().as_bytes()
    {
        Ok(())
    } else {
        Err("canonical bootstrap Complete conflicts with reconciled Loom topology".to_owned())
    }
}

fn require_web_complete(record: &CompleteRecord) -> Result<(), String> {
    if record
        .packages()
        .iter()
        .any(|package| package.logical_name().starts_with(WEB_INVENTORY_PREFIX))
    {
        Ok(())
    } else {
        Err("canonical bootstrap Complete predates Web bootstrap; delete the disposable state and restart blank".to_owned())
    }
}

fn reconcile_user(
    invites: &K1Invites,
    users: &K1Users,
    identity: &BootstrapIdentity,
) -> Result<User, String> {
    if let Some(existing) = users.find_by_username(identity.username())? {
        require_matching_user(&existing, identity)?;
        return Ok(existing);
    }
    let (_, invite) = invites.create()?;
    let candidate = NewUser::new(
        identity.username(),
        identity.full_name(),
        identity.public_key(),
        kcode_k1_terms::REVISION,
        kcode_k1_terms::sha256(),
        identity.message_signature(),
    )?;
    let user = users.register(&invite, candidate)?;
    require_matching_user(&user, identity)?;
    Ok(user)
}

fn require_matching_user(user: &User, identity: &BootstrapIdentity) -> Result<(), String> {
    if user.username() == identity.username()
        && user.full_name() == identity.full_name()
        && user.public_key() == identity.public_key()
        && user.tos_revision() == kcode_k1_terms::REVISION
        && user.tos_digest() == kcode_k1_terms::sha256()
        && user.acceptance_signature() == identity.message_signature()
    {
        Ok(())
    } else {
        Err("existing bootstrap Account conflicts with entered identity".to_owned())
    }
}

fn rust_state_inventory(
    imported: Vec<kcode_k1_rust_bootstrap_import::ImportedPackage>,
) -> Result<Vec<StatePackage>, String> {
    imported
        .into_iter()
        .map(|package| {
            StatePackage::new(package.logical_name().to_owned(), package.version().clone())
        })
        .collect()
}

fn web_state_inventory(imported: Vec<WebImportedPackage>) -> Result<Vec<StatePackage>, String> {
    imported
        .into_iter()
        .map(|package| {
            StatePackage::new(
                format!("{WEB_INVENTORY_PREFIX}{}", package.name()),
                package.version().clone(),
            )
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn web_inventory_namespace_and_import_log_are_bootstrap_owned() {
        assert!(WEB_INVENTORY_PREFIX.contains(':'));
        assert_eq!(WEB_IMPORT_LOG, "bootstrap/k1-web-import.log");
        assert!(BLANK_RESTART.contains("restart"));
    }
}