Skip to main content

kcode_k1_loom_bootstrap/
lib.rs

1#![doc = include_str!("../Documentation.md")]
2#![forbid(unsafe_code)]
3
4use kcode_k1_access::K1Access;
5use kcode_k1_access_kmap::K1AccessKmap;
6use kcode_k1_access_launch_nodes::{ModelId, TxId, UserId};
7use kcode_k1_access_profiles::K1AccessProfiles;
8use kcode_k1_bootstrap_identity::{BootstrapIdentity, prompt};
9use kcode_k1_bootstrap_state::{
10    BeginRecord, BootstrapStatus, CompleteRecord, ImportedPackage as StatePackage, K1BootstrapState,
11};
12use kcode_k1_groups::{GroupId, K1Groups};
13use kcode_k1_invites::K1Invites;
14use kcode_k1_launch_nodes::LaunchNodes;
15use kcode_k1_loom_bootstrap_topology::{
16    BootstrapTopology, TopologyServices, ensure as ensure_topology,
17};
18use kcode_k1_rust_bootstrap_import::RustBootstrapImporter;
19use kcode_k1_rust_projection::K1RustProjection;
20use kcode_k1_users::{K1Users, NewUser, User};
21use kcode_k1_web_bootstrap_archive::read as read_web;
22use kcode_k1_web_bootstrap_import::{
23    ImportedPackage as WebImportedPackage, WebBootstrapImporter, write_preflight_log,
24};
25use std::path::Path;
26
27const WEB_INVENTORY_PREFIX: &str = "web:";
28const WEB_IMPORT_LOG: &str = "bootstrap/k1-web-import.log";
29const KENNEDY_GROUP: &str = "kennedy-devs";
30const BLANK_RESTART: &str = "Loom bootstrap previously began without completing; delete the disposable blank-state data and restart";
31
32pub struct BootstrapServices<'a> {
33    pub state: &'a K1BootstrapState,
34    pub invites: &'a K1Invites,
35    pub users: &'a K1Users,
36    pub groups: &'a K1Groups,
37    pub profiles: &'a K1AccessProfiles,
38    pub access_kmap: &'a K1AccessKmap,
39    pub access_launch_nodes: &'a kcode_k1_access_launch_nodes::K1AccessLaunchNodes,
40    pub launch_nodes: &'a LaunchNodes,
41    pub rust_projection: &'a K1RustProjection,
42    pub model: ModelId,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub struct BootstrapDeveloperGroups {
47    loom_devs: TxId,
48    kennedy_devs: TxId,
49}
50
51impl BootstrapDeveloperGroups {
52    pub const fn loom_devs(&self) -> TxId {
53        self.loom_devs
54    }
55
56    pub const fn kennedy_devs(&self) -> TxId {
57        self.kennedy_devs
58    }
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct BootstrapResult {
63    record: CompleteRecord,
64    completed_now: bool,
65    developer_groups: BootstrapDeveloperGroups,
66}
67
68impl BootstrapResult {
69    pub fn record(&self) -> &CompleteRecord {
70        &self.record
71    }
72
73    pub const fn completed_now(&self) -> bool {
74        self.completed_now
75    }
76
77    pub const fn developer_groups(&self) -> BootstrapDeveloperGroups {
78        self.developer_groups
79    }
80}
81
82pub fn ensure_with_topology(
83    root: &Path,
84    access: &K1Access,
85    web_importer: &WebBootstrapImporter,
86    services: BootstrapServices<'_>,
87) -> Result<BootstrapResult, String> {
88    match services.state.status()? {
89        BootstrapStatus::Complete { record, .. } => {
90            let developer_groups = completed_developer_groups(&record, services.groups)?;
91            return Ok(result(record, false, developer_groups));
92        }
93        BootstrapStatus::Begun { .. } => return Err(BLANK_RESTART.to_owned()),
94        BootstrapStatus::Empty => {}
95    }
96
97    let rust_importer = RustBootstrapImporter::open(
98        &root.join("bootstrap/k1-rust-code.zip"),
99        &root.join("bootstrap/k1-rust-import.log"),
100    )?;
101    let web = read_web(&root.join("bootstrap/k1-web-ui.zip"))?;
102    let web_import_log = root.join(WEB_IMPORT_LOG);
103    write_preflight_log(&web.archive, &web_import_log)?;
104
105    let identity = prompt(kcode_k1_terms::text().as_bytes())?;
106    let begin = BeginRecord::new(
107        identity.username().to_owned(),
108        identity.full_name().to_owned(),
109        identity.public_key(),
110    );
111    services.state.begin(begin.clone())?;
112
113    let user = reconcile_user(services.invites, services.users, &identity)?;
114    let account_user_bytes = *user.user_id().as_tx_id().as_bytes();
115    let first_user = UserId::from_tx_id(TxId::from_bytes(account_user_bytes));
116    let topology = reconcile_topology(first_user, access, &services)?;
117    let loom_authority = *topology.loom_group().txid().as_bytes();
118
119    let imported_rust = rust_importer.import_all(services.rust_projection, loom_authority, root)?;
120    let imported_web = web_importer.import_all(
121        &web.archive,
122        loom_authority,
123        account_user_bytes,
124        &web_import_log,
125    )?;
126
127    let mut inventory = rust_state_inventory(imported_rust)?;
128    inventory.extend(web_state_inventory(imported_web)?);
129    let record = CompleteRecord::new(
130        begin,
131        account_user_bytes,
132        loom_authority,
133        *topology.loom_profile().txid().as_bytes(),
134        *topology.loom_root().txid().as_bytes(),
135        inventory,
136    )?;
137    services.state.complete(record.clone())?;
138    let developer_groups = BootstrapDeveloperGroups {
139        loom_devs: TxId::from_bytes(*topology.loom_group().txid().as_bytes()),
140        kennedy_devs: TxId::from_bytes(*topology.kennedy_group().txid().as_bytes()),
141    };
142    Ok(result(record, true, developer_groups))
143}
144
145fn result(
146    record: CompleteRecord,
147    completed_now: bool,
148    developer_groups: BootstrapDeveloperGroups,
149) -> BootstrapResult {
150    BootstrapResult {
151        record,
152        completed_now,
153        developer_groups,
154    }
155}
156
157fn completed_developer_groups(
158    record: &CompleteRecord,
159    groups: &K1Groups,
160) -> Result<BootstrapDeveloperGroups, String> {
161    let first_user = UserId::from_tx_id(TxId::from_bytes(record.user_id()));
162    let kennedy_group = find_group(groups, first_user, KENNEDY_GROUP)?;
163    Ok(BootstrapDeveloperGroups {
164        loom_devs: TxId::from_bytes(record.group_id()),
165        kennedy_devs: TxId::from_bytes(*kennedy_group.txid().as_bytes()),
166    })
167}
168
169fn find_group(groups: &K1Groups, first_user: UserId, name: &str) -> Result<GroupId, String> {
170    let mut matches = Vec::new();
171    for id in groups.groups_for_user(first_user)? {
172        if id.sentinel().is_none()
173            && let Some(group) = groups.get(id)?
174            && group.name().as_str() == name
175        {
176            matches.push(id);
177        }
178    }
179    one_named_group(matches, name)
180}
181
182fn one_named_group<T>(mut matches: Vec<T>, name: &str) -> Result<T, String> {
183    match matches.len() {
184        0 => Err(format!("{name} group is unavailable")),
185        1 => Ok(matches.pop().expect("one matching group")),
186        _ => Err(format!("multiple {name} groups are visible")),
187    }
188}
189
190fn reconcile_topology(
191    first_user: UserId,
192    access: &K1Access,
193    services: &BootstrapServices<'_>,
194) -> Result<BootstrapTopology, String> {
195    ensure_topology(
196        first_user,
197        TopologyServices {
198            groups: services.groups,
199            profiles: services.profiles,
200            access,
201            access_kmap: services.access_kmap,
202            access_launch_nodes: services.access_launch_nodes,
203            launch_nodes: services.launch_nodes,
204            model: services.model,
205        },
206    )
207}
208
209fn reconcile_user(
210    invites: &K1Invites,
211    users: &K1Users,
212    identity: &BootstrapIdentity,
213) -> Result<User, String> {
214    if let Some(existing) = users.find_by_username(identity.username())? {
215        require_matching_user(&existing, identity)?;
216        return Ok(existing);
217    }
218    let (_, invite) = invites.create()?;
219    let candidate = NewUser::new(
220        identity.username(),
221        identity.full_name(),
222        identity.public_key(),
223        kcode_k1_terms::REVISION,
224        kcode_k1_terms::sha256(),
225        identity.message_signature(),
226    )?;
227    let user = users.register(&invite, candidate)?;
228    require_matching_user(&user, identity)?;
229    Ok(user)
230}
231
232fn require_matching_user(user: &User, identity: &BootstrapIdentity) -> Result<(), String> {
233    if user.username() == identity.username()
234        && user.full_name() == identity.full_name()
235        && user.public_key() == identity.public_key()
236        && user.tos_revision() == kcode_k1_terms::REVISION
237        && user.tos_digest() == kcode_k1_terms::sha256()
238        && user.acceptance_signature() == identity.message_signature()
239    {
240        Ok(())
241    } else {
242        Err("existing bootstrap Account conflicts with entered identity".to_owned())
243    }
244}
245
246fn rust_state_inventory(
247    imported: Vec<kcode_k1_rust_bootstrap_import::ImportedPackage>,
248) -> Result<Vec<StatePackage>, String> {
249    imported
250        .into_iter()
251        .map(|package| {
252            StatePackage::new(package.logical_name().to_owned(), package.version().clone())
253        })
254        .collect()
255}
256
257fn web_state_inventory(imported: Vec<WebImportedPackage>) -> Result<Vec<StatePackage>, String> {
258    imported
259        .into_iter()
260        .map(|package| {
261            StatePackage::new(
262                format!("{WEB_INVENTORY_PREFIX}{}", package.name()),
263                package.version().clone(),
264            )
265        })
266        .collect()
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn web_inventory_namespace_and_import_log_are_bootstrap_owned() {
275        assert!(WEB_INVENTORY_PREFIX.contains(':'));
276        assert_eq!(WEB_IMPORT_LOG, "bootstrap/k1-web-import.log");
277        assert_eq!(KENNEDY_GROUP, "kennedy-devs");
278        assert!(BLANK_RESTART.contains("restart"));
279    }
280
281    #[test]
282    fn completed_group_lookup_requires_one_name_match() {
283        assert_eq!(one_named_group(vec![7_u8], KENNEDY_GROUP).unwrap(), 7);
284        assert!(
285            one_named_group::<u8>(Vec::new(), KENNEDY_GROUP)
286                .unwrap_err()
287                .contains("unavailable")
288        );
289        assert!(
290            one_named_group(vec![1_u8, 2_u8], KENNEDY_GROUP)
291                .unwrap_err()
292                .contains("multiple")
293        );
294    }
295}