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