1use 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#[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
68pub(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
107pub async fn create_and_install_canister(
121 role: &CanisterRole,
122 parent_pid: Principal,
123 extra_arg: Option<Vec<u8>>,
124) -> Result<Principal, Error> {
125 WasmOps::try_get(role)?;
127
128 let pid = allocate_canister(role).await?;
130
131 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
139pub async fn delete_canister(pid: Principal) -> Result<(), Error> {
154 OpsError::require_root()?;
155
156 super::uninstall_code(pid).await?;
158
159 super::delete_canister(pid).await?;
161
162 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
182pub 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
191pub async fn allocate_canister(role: &CanisterRole) -> Result<Principal, Error> {
201 let cfg = ConfigOps::current_subnet_canister(role);
203 let target = cfg.initial_cycles;
204
205 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 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
245async 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); 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#[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 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 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}