canic_core/ops/bootstrap/
root.rs

1use crate::{
2    Error,
3    cdk::api::{canister_self, trap},
4    log::Topic,
5    ops::{
6        config::ConfigOps,
7        ic::{Network, build_network, try_get_current_subnet_pid},
8        pool::{PoolOps, pool_import_canister},
9        prelude::*,
10        rpc::{CreateCanisterParent, create_canister_request},
11        storage::{env::EnvOps, topology::SubnetCanisterRegistryOps},
12    },
13};
14
15/// Initializes the subnet identifier for the root canister.
16///
17/// This attempts to resolve the subnet ID via the NNS registry and records it
18/// into durable environment state. This value is required by downstream
19/// topology, placement, and orchestration logic.
20///
21/// If the registry is unavailable (e.g. PocketIC or local testing), the
22/// canister's own principal is used as a deterministic fallback.
23pub async fn root_set_subnet_id() {
24    // Preferred path: query the NNS registry for the subnet this canister
25    // currently belongs to.
26    let subnet_result = try_get_current_subnet_pid().await;
27    match subnet_result {
28        Ok(Some(subnet_pid)) => {
29            EnvOps::set_subnet_pid(subnet_pid);
30            return;
31        }
32        Ok(None) => {
33            if build_network() == Some(Network::Ic) {
34                let msg = "try_get_current_subnet_pid returned None on ic; refusing to fall back";
35                log!(Topic::Topology, Error, "{msg}");
36                trap(msg);
37            }
38        }
39        Err(err) => {
40            if build_network() == Some(Network::Ic) {
41                let msg = format!("try_get_current_subnet_pid failed on ic: {err}");
42                log!(Topic::Topology, Error, "{msg}");
43                trap(&msg);
44            }
45        }
46    }
47
48    // Fallback path: environments without a registry (e.g. PocketIC).
49    // Using self ensures a stable, non-null subnet identifier.
50    let fallback = canister_self();
51    EnvOps::set_subnet_pid(fallback);
52
53    log!(
54        Topic::Topology,
55        Info,
56        "try_get_current_subnet_pid unavailable; using self as subnet: {fallback}"
57    );
58}
59
60/// Import any statically configured pool canisters for this subnet.
61///
62/// Import failures are summarized so bootstrap can continue.
63pub async fn root_import_pool_from_config() {
64    let subnet_cfg = ConfigOps::current_subnet();
65    let import_list = match build_network() {
66        Some(Network::Local) => subnet_cfg.pool.import.local,
67        Some(Network::Ic) => subnet_cfg.pool.import.ic,
68        None => {
69            log!(
70                Topic::CanisterPool,
71                Warn,
72                "pool import skipped: build network not set"
73            );
74            return;
75        }
76    };
77
78    if import_list.is_empty() {
79        return;
80    }
81
82    let mut attempted = 0_u64;
83    let mut imported = 0_u64;
84    let mut skipped = 0_u64;
85    let mut failed = 0_u64;
86
87    for pid in import_list {
88        attempted += 1;
89        match pool_import_canister(pid).await {
90            Ok(()) => {
91                if PoolOps::contains(&pid) {
92                    imported += 1;
93                } else {
94                    skipped += 1;
95                }
96            }
97            Err(_) => {
98                failed += 1;
99            }
100        }
101    }
102
103    log!(
104        Topic::CanisterPool,
105        Info,
106        "pool import summary: configured={attempted}, imported={imported}, skipped={skipped}, failed={failed}"
107    );
108}
109
110/// Ensures all statically configured canisters for this subnet exist.
111///
112/// This function:
113/// - Reads the subnet configuration
114/// - Issues creation requests for any auto-create roles
115/// - Emits a summary of the resulting topology
116///
117/// Intended to run during root bootstrap or upgrade flows.
118/// Safe to re-run: skips roles that already exist in the subnet registry.
119pub async fn root_create_canisters() -> Result<(), Error> {
120    // Load the effective configuration for the current subnet.
121    let subnet_cfg = ConfigOps::current_subnet();
122
123    // Creation pass: ensure all auto-create canister roles exist.
124    for role in &subnet_cfg.auto_create {
125        if let Some(existing) = SubnetCanisterRegistryOps::get_type(role) {
126            log!(
127                Topic::Init,
128                Info,
129                "auto_create: {role} already registered as {}, skipping",
130                existing.pid
131            );
132            continue;
133        }
134
135        create_canister_request::<()>(role, CreateCanisterParent::Root, None).await?;
136    }
137
138    // Reporting pass: emit the current topology for observability/debugging.
139    for canister in SubnetCanisterRegistryOps::export() {
140        log!(Topic::Init, Info, "🥫 {} ({})", canister.role, canister.pid);
141    }
142
143    Ok(())
144}