canic_core/ops/ic/
provision.rs

1// =============================================================================
2// PROVISIONING (ROOT ORCHESTRATOR HELPERS)
3// =============================================================================
4
5//! Provisioning helpers for creating, installing, and tearing down canisters.
6//!
7//! These routines bundle the multi-phase orchestration that root performs when
8//! scaling out the topology: reserving cycles, recording registry state,
9//! installing WASM modules, and cascading state updates to descendants.
10
11use crate::{
12    Error,
13    cdk::{api::canister_self, mgmt::CanisterInstallMode},
14    config::Config,
15    log::Topic,
16    ops::{
17        OpsError,
18        config::ConfigOps,
19        ic::IcOpsError,
20        orchestration::cascade::state::StateBundle,
21        pool::PoolOps,
22        prelude::*,
23        storage::{
24            CanisterInitPayload,
25            directory::{AppDirectoryOps, SubnetDirectoryOps},
26            env::{EnvData, EnvOps},
27            topology::SubnetCanisterRegistryOps,
28        },
29        wasm::WasmOps,
30    },
31    types::Cycles,
32};
33use candid::Principal;
34use thiserror::Error as ThisError;
35
36pub(crate) fn build_nonroot_init_payload(
37    role: &CanisterRole,
38    parent_pid: Principal,
39) -> CanisterInitPayload {
40    let env = EnvData {
41        prime_root_pid: Some(EnvOps::prime_root_pid()),
42        subnet_role: Some(EnvOps::subnet_role()),
43        subnet_pid: Some(EnvOps::subnet_pid()),
44        root_pid: Some(EnvOps::root_pid()),
45        canister_role: Some(role.clone()),
46        parent_pid: Some(parent_pid),
47    };
48
49    CanisterInitPayload::new(env, AppDirectoryOps::export(), SubnetDirectoryOps::export())
50}
51
52///
53/// ProvisionOpsError
54///
55
56#[derive(Debug, ThisError)]
57pub enum ProvisionOpsError {
58    #[error("install failed for {pid}: {source}")]
59    InstallFailed { pid: Principal, source: Error },
60}
61
62impl From<ProvisionOpsError> for Error {
63    fn from(err: ProvisionOpsError) -> Self {
64        IcOpsError::from(err).into()
65    }
66}
67
68//
69// ===========================================================================
70// DIRECTORY SYNC
71// ===========================================================================
72//
73
74/// Rebuild AppDirectory and SubnetDirectory from the registry,
75/// import them directly, and return the resulting state bundle.
76///
77/// When `updated_role` is provided, only include the sections that
78/// list that role.
79pub(crate) async fn rebuild_directories_from_registry(
80    updated_role: Option<&CanisterRole>,
81) -> StateBundle {
82    let mut bundle = StateBundle::default();
83    let cfg = Config::get();
84
85    let include_app = updated_role.is_none_or(|role| cfg.app_directory.contains(role));
86
87    let include_subnet = updated_role.is_none_or(|role| {
88        let subnet_cfg = ConfigOps::current_subnet();
89        subnet_cfg.subnet_directory.contains(role)
90    });
91
92    if include_app {
93        let app_view = AppDirectoryOps::root_build_view();
94        AppDirectoryOps::import(app_view.clone());
95        bundle.app_directory = Some(app_view);
96    }
97
98    if include_subnet {
99        let subnet_view = SubnetDirectoryOps::root_build_view();
100        SubnetDirectoryOps::import(subnet_view.clone());
101        bundle.subnet_directory = Some(subnet_view);
102    }
103
104    bundle
105}
106
107//
108// ===========================================================================
109// HIGH-LEVEL FLOW
110// ===========================================================================
111//
112
113/// Create and install a new canister of the requested type beneath `parent`.
114///
115/// PHASES:
116/// 1. Allocate a canister ID and cycles (preferring the pool)
117/// 2. Install WASM + bootstrap initial state
118/// 3. Register canister in SubnetCanisterRegistry
119/// 4. Cascade topology + sync directories
120pub async fn create_and_install_canister(
121    role: &CanisterRole,
122    parent_pid: Principal,
123    extra_arg: Option<Vec<u8>>,
124) -> Result<Principal, Error> {
125    // must have WASM module registered
126    WasmOps::try_get(role)?;
127
128    // Phase 1: allocation
129    let pid = allocate_canister(role).await?;
130
131    // Phase 2: installation
132    if let Err(err) = install_canister(pid, role, parent_pid, extra_arg).await {
133        return Err(ProvisionOpsError::InstallFailed { pid, source: err }.into());
134    }
135
136    Ok(pid)
137}
138
139//
140// ===========================================================================
141// DELETION
142// ===========================================================================
143//
144
145/// Delete an existing canister.
146///
147/// PHASES:
148/// 0. Uninstall code
149/// 1. Delete via management canister
150/// 2. Remove from SubnetCanisterRegistry
151/// 3. Cascade topology
152/// 4. Sync directories
153pub async fn delete_canister(pid: Principal) -> Result<(), Error> {
154    OpsError::require_root()?;
155
156    // Phase 0: uninstall code
157    super::uninstall_code(pid).await?;
158
159    // Phase 1: delete the canister
160    super::delete_canister(pid).await?;
161
162    // Phase 2: remove registry record
163    let removed_entry = SubnetCanisterRegistryOps::remove(&pid);
164    match &removed_entry {
165        Some(c) => log!(
166            Topic::CanisterLifecycle,
167            Ok,
168            "🗑️ delete_canister: {} ({})",
169            pid,
170            c.role
171        ),
172        None => log!(
173            Topic::CanisterLifecycle,
174            Warn,
175            "🗑️ delete_canister: {pid} not in registry"
176        ),
177    }
178
179    Ok(())
180}
181
182/// Uninstall code from a canister without deleting it.
183pub async fn uninstall_canister(pid: Principal) -> Result<(), Error> {
184    super::uninstall_code(pid).await?;
185
186    log!(Topic::CanisterLifecycle, Ok, "🗑️ uninstall_canister: {pid}");
187
188    Ok(())
189}
190
191//
192// ===========================================================================
193// PHASE 1 — ALLOCATION (Pool → Create)
194// ===========================================================================
195//
196
197/// Allocate a canister ID and ensure it meets the initial cycle target.
198///
199/// Reuses a canister from the pool if available; otherwise creates a new one.
200pub async fn allocate_canister(role: &CanisterRole) -> Result<Principal, Error> {
201    // use ConfigOps for a clean, ops-layer config lookup
202    let cfg = ConfigOps::current_subnet_canister(role);
203    let target = cfg.initial_cycles;
204
205    // Reuse from pool
206    if let Some((pid, _)) = PoolOps::pop_ready() {
207        let mut current = super::get_cycles(pid).await?;
208
209        if current < target {
210            let missing = target.to_u128().saturating_sub(current.to_u128());
211            if missing > 0 {
212                super::deposit_cycles(pid, missing).await?;
213                current = Cycles::new(current.to_u128() + missing);
214
215                log!(
216                    Topic::CanisterPool,
217                    Ok,
218                    "⚡ allocate_canister: topped up {pid} by {} to meet target {}",
219                    Cycles::from(missing),
220                    target
221                );
222            }
223        }
224
225        log!(
226            Topic::CanisterPool,
227            Ok,
228            "⚡ allocate_canister: reusing {pid} from pool (current {current})"
229        );
230
231        return Ok(pid);
232    }
233
234    // Create new canister
235    let pid = create_canister_with_configured_controllers(target).await?;
236    log!(
237        Topic::CanisterPool,
238        Info,
239        "⚡ allocate_canister: pool empty"
240    );
241
242    Ok(pid)
243}
244
245/// Create a fresh canister on the IC with the configured controllers.
246async fn create_canister_with_configured_controllers(cycles: Cycles) -> Result<Principal, Error> {
247    let root = canister_self();
248    let mut controllers = Config::get().controllers.clone();
249    controllers.push(root); // root always controls
250
251    let pid = super::create_canister(controllers, cycles.clone()).await?;
252
253    log!(
254        Topic::CanisterLifecycle,
255        Ok,
256        "⚡ create_canister: {pid} ({cycles})"
257    );
258
259    Ok(pid)
260}
261
262//
263// ===========================================================================
264// PHASE 2 — INSTALLATION
265// ===========================================================================
266//
267
268/// Install WASM and initial state into a new canister.
269#[allow(clippy::cast_precision_loss)]
270async fn install_canister(
271    pid: Principal,
272    role: &CanisterRole,
273    parent_pid: Principal,
274    extra_arg: Option<Vec<u8>>,
275) -> Result<(), Error> {
276    // Fetch and register WASM
277    let wasm = WasmOps::try_get(role)?;
278
279    let payload = build_nonroot_init_payload(role, parent_pid);
280    let module_hash = wasm.module_hash();
281
282    // Register before install so init hooks can observe the registry; roll back on failure.
283    // otherwise if the init() tries to create a canister via root, it will panic
284    SubnetCanisterRegistryOps::register(pid, role, parent_pid, module_hash.clone());
285
286    if let Err(err) = super::install_canic_code(
287        CanisterInstallMode::Install,
288        pid,
289        wasm.bytes(),
290        payload,
291        extra_arg,
292    )
293    .await
294    {
295        let removed = SubnetCanisterRegistryOps::remove(&pid);
296        if removed.is_none() {
297            log!(
298                Topic::CanisterLifecycle,
299                Warn,
300                "⚠️ install_canister rollback: {pid} missing from registry after failed install"
301            );
302        }
303
304        return Err(err);
305    }
306
307    log!(
308        Topic::CanisterLifecycle,
309        Ok,
310        "⚡ install_canister: {pid} ({role}, {:.2} KiB)",
311        wasm.len() as f64 / 1_024.0,
312    );
313
314    Ok(())
315}