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::get();
89
90 let include_app = updated_role.is_none_or(|role| cfg.app_directory.contains(role));
92 let include_subnet = if let Some(role) = updated_role {
93 let subnet_cfg = ConfigOps::current_subnet()?;
94 subnet_cfg.subnet_directory.contains(role)
95 } else {
96 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
114pub async fn create_and_install_canister(
128 role: &CanisterRole,
129 parent_pid: Principal,
130 extra_arg: Option<Vec<u8>>,
131) -> Result<Principal, Error> {
132 WasmOps::try_get(role)?;
134
135 let pid = allocate_canister(role).await?;
137
138 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
146pub async fn delete_canister(pid: Principal) -> Result<(), Error> {
161 OpsError::require_root()?;
162
163 super::uninstall_code(pid).await?;
165
166 super::delete_canister(pid).await?;
168
169 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
189pub 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
198pub async fn allocate_canister(role: &CanisterRole) -> Result<Principal, Error> {
208 let cfg = ConfigOps::current_subnet_canister(role)?;
210 let target = cfg.initial_cycles;
211
212 if let Some((pid, _)) = PoolOps::pop_ready() {
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::CanisterPool,
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::CanisterPool,
234 Ok,
235 "⚡ allocate_canister: reusing {pid} from pool (current {current})"
236 );
237
238 return Ok(pid);
239 }
240
241 let pid = create_canister_with_configured_controllers(target).await?;
243 log!(
244 Topic::CanisterPool,
245 Info,
246 "⚡ allocate_canister: pool empty"
247 );
248
249 Ok(pid)
250}
251
252async fn create_canister_with_configured_controllers(cycles: Cycles) -> Result<Principal, Error> {
254 let root = canister_self();
255 let mut controllers = Config::get().controllers.clone();
256 controllers.push(root); let pid = super::create_canister(controllers, cycles.clone()).await?;
259
260 log!(
261 Topic::CanisterLifecycle,
262 Ok,
263 "⚡ create_canister: {pid} ({cycles})"
264 );
265
266 Ok(pid)
267}
268
269#[allow(clippy::cast_precision_loss)]
277async fn install_canister(
278 pid: Principal,
279 role: &CanisterRole,
280 parent_pid: Principal,
281 extra_arg: Option<Vec<u8>>,
282) -> Result<(), Error> {
283 let wasm = WasmOps::try_get(role)?;
285
286 let payload = build_nonroot_init_payload(role, parent_pid)?;
287
288 let module_hash = wasm.module_hash();
289
290 SubnetCanisterRegistryOps::register(pid, role, parent_pid, module_hash.clone());
293
294 if let Err(err) = super::install_canic_code(
295 CanisterInstallMode::Install,
296 pid,
297 wasm.bytes(),
298 payload,
299 extra_arg,
300 )
301 .await
302 {
303 let removed = SubnetCanisterRegistryOps::remove(&pid);
304 if removed.is_none() {
305 log!(
306 Topic::CanisterLifecycle,
307 Warn,
308 "⚠️ install_canister rollback: {pid} missing from registry after failed install"
309 );
310 }
311
312 return Err(err);
313 }
314
315 log!(
316 Topic::CanisterLifecycle,
317 Ok,
318 "⚡ install_canister: {pid} ({role}, {:.2} KiB)",
319 wasm.len() as f64 / 1_024.0,
320 );
321
322 Ok(())
323}