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) -> 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#[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
75pub(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 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 .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
117pub async fn create_and_install_canister(
131 role: &CanisterRole,
132 parent_pid: Principal,
133 extra_arg: Option<Vec<u8>>,
134) -> Result<Principal, Error> {
135 WasmOps::try_get(role)?;
137
138 let pid = allocate_canister(role).await?;
140
141 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
149pub async fn delete_canister(pid: Principal) -> Result<(), Error> {
164 OpsError::require_root()?;
165
166 super::uninstall_code(pid).await?;
168
169 super::delete_canister(pid).await?;
171
172 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
192pub 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
201pub async fn allocate_canister(role: &CanisterRole) -> Result<Principal, Error> {
211 let cfg = ConfigOps::current_subnet_canister(role)?;
213 let target = cfg.initial_cycles;
214
215 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 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
255async 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); 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#[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 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 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}