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