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, pool_import_canister},
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, source) = allocate_canister_with_source(role).await?;
130
131    // Phase 2: installation
132    if let Err(err) = install_canister(pid, role, parent_pid, extra_arg).await {
133        if source == AllocationSource::Pool {
134            if let Err(recycle_err) = pool_import_canister(pid).await {
135                log!(
136                    Topic::CanisterPool,
137                    Warn,
138                    "failed to recycle pool canister after install failure: {pid} ({recycle_err})"
139                );
140            }
141        } else if let Err(delete_err) = delete_canister(pid).await {
142            log!(
143                Topic::CanisterLifecycle,
144                Warn,
145                "failed to delete canister after install failure: {pid} ({delete_err})"
146            );
147        }
148        return Err(ProvisionOpsError::InstallFailed { pid, source: err }.into());
149    }
150
151    Ok(pid)
152}
153
154//
155// ===========================================================================
156// DELETION
157// ===========================================================================
158//
159
160/// Delete an existing canister.
161///
162/// PHASES:
163/// 0. Uninstall code
164/// 1. Delete via management canister
165/// 2. Remove from SubnetCanisterRegistry
166/// 3. Cascade topology
167/// 4. Sync directories
168pub async fn delete_canister(pid: Principal) -> Result<(), Error> {
169    OpsError::require_root()?;
170
171    // Phase 0: uninstall code
172    super::uninstall_code(pid).await?;
173
174    // Phase 1: delete the canister
175    super::delete_canister(pid).await?;
176
177    // Phase 2: remove registry record
178    let removed_entry = SubnetCanisterRegistryOps::remove(&pid);
179    match &removed_entry {
180        Some(c) => log!(
181            Topic::CanisterLifecycle,
182            Ok,
183            "🗑️ delete_canister: {} ({})",
184            pid,
185            c.role
186        ),
187        None => log!(
188            Topic::CanisterLifecycle,
189            Warn,
190            "🗑️ delete_canister: {pid} not in registry"
191        ),
192    }
193
194    Ok(())
195}
196
197/// Uninstall code from a canister without deleting it.
198pub async fn uninstall_canister(pid: Principal) -> Result<(), Error> {
199    super::uninstall_code(pid).await?;
200
201    log!(Topic::CanisterLifecycle, Ok, "🗑️ uninstall_canister: {pid}");
202
203    Ok(())
204}
205
206//
207// ===========================================================================
208// PHASE 1 — ALLOCATION (Pool → Create)
209// ===========================================================================
210//
211
212/// Allocate a canister ID and ensure it meets the initial cycle target.
213///
214/// Reuses a canister from the pool if available; otherwise creates a new one.
215pub async fn allocate_canister(role: &CanisterRole) -> Result<Principal, Error> {
216    let (pid, _) = allocate_canister_with_source(role).await?;
217    Ok(pid)
218}
219
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221enum AllocationSource {
222    Pool,
223    New,
224}
225
226async fn allocate_canister_with_source(
227    role: &CanisterRole,
228) -> Result<(Principal, AllocationSource), Error> {
229    // use ConfigOps for a clean, ops-layer config lookup
230    let cfg = ConfigOps::current_subnet_canister(role);
231    let target = cfg.initial_cycles;
232
233    // Reuse from pool
234    if let Some((pid, _)) = PoolOps::pop_ready() {
235        let mut current = super::get_cycles(pid).await?;
236
237        if current < target {
238            let missing = target.to_u128().saturating_sub(current.to_u128());
239            if missing > 0 {
240                super::deposit_cycles(pid, missing).await?;
241                current = Cycles::new(current.to_u128() + missing);
242
243                log!(
244                    Topic::CanisterPool,
245                    Ok,
246                    "⚡ allocate_canister: topped up {pid} by {} to meet target {}",
247                    Cycles::from(missing),
248                    target
249                );
250            }
251        }
252
253        log!(
254            Topic::CanisterPool,
255            Ok,
256            "⚡ allocate_canister: reusing {pid} from pool (current {current})"
257        );
258
259        return Ok((pid, AllocationSource::Pool));
260    }
261
262    // Create new canister
263    let pid = create_canister_with_configured_controllers(target).await?;
264    log!(
265        Topic::CanisterPool,
266        Info,
267        "⚡ allocate_canister: pool empty"
268    );
269
270    Ok((pid, AllocationSource::New))
271}
272
273/// Create a fresh canister on the IC with the configured controllers.
274async fn create_canister_with_configured_controllers(cycles: Cycles) -> Result<Principal, Error> {
275    let root = canister_self();
276    let mut controllers = Config::get().controllers.clone();
277    controllers.push(root); // root always controls
278
279    let pid = super::create_canister(controllers, cycles.clone()).await?;
280
281    log!(
282        Topic::CanisterLifecycle,
283        Ok,
284        "⚡ create_canister: {pid} ({cycles})"
285    );
286
287    Ok(pid)
288}
289
290//
291// ===========================================================================
292// PHASE 2 — INSTALLATION
293// ===========================================================================
294//
295
296/// Install WASM and initial state into a new canister.
297#[allow(clippy::cast_precision_loss)]
298async fn install_canister(
299    pid: Principal,
300    role: &CanisterRole,
301    parent_pid: Principal,
302    extra_arg: Option<Vec<u8>>,
303) -> Result<(), Error> {
304    // Fetch and register WASM
305    let wasm = WasmOps::try_get(role)?;
306
307    let payload = build_nonroot_init_payload(role, parent_pid);
308    let module_hash = wasm.module_hash();
309
310    // Register before install so init hooks can observe the registry; roll back on failure.
311    // otherwise if the init() tries to create a canister via root, it will panic
312    SubnetCanisterRegistryOps::register(pid, role, parent_pid, module_hash.clone());
313
314    if let Err(err) = super::install_canic_code(
315        CanisterInstallMode::Install,
316        pid,
317        wasm.bytes(),
318        payload,
319        extra_arg,
320    )
321    .await
322    {
323        let removed = SubnetCanisterRegistryOps::remove(&pid);
324        if removed.is_none() {
325            log!(
326                Topic::CanisterLifecycle,
327                Warn,
328                "⚠️ install_canister rollback: {pid} missing from registry after failed install"
329            );
330        }
331
332        return Err(err);
333    }
334
335    log!(
336        Topic::CanisterLifecycle,
337        Ok,
338        "⚡ install_canister: {pid} ({role}, {:.2} KiB)",
339        wasm.len() as f64 / 1_024.0,
340    );
341
342    Ok(())
343}